From 84eab1a1113600fa9e461722dab0f921a7c56b8a Mon Sep 17 00:00:00 2001 From: Tsaqif <tsaiinkwa@yahoo.com> Date: Sun, 5 Nov 2023 10:42:34 +0700 Subject: [PATCH 001/580] Add CurrencySelectionList component and use it in IOU and WS currency settings Signed-off-by: Tsaqif <tsaiinkwa@yahoo.com> --- src/components/CurrencySelectionList.js | 92 +++++++++++++++++++ src/pages/iou/IOUCurrencySelection.js | 74 ++------------- .../WorkspaceSettingsCurrencyPage.js | 67 ++------------ 3 files changed, 106 insertions(+), 127 deletions(-) create mode 100644 src/components/CurrencySelectionList.js diff --git a/src/components/CurrencySelectionList.js b/src/components/CurrencySelectionList.js new file mode 100644 index 000000000000..1c0733ae9927 --- /dev/null +++ b/src/components/CurrencySelectionList.js @@ -0,0 +1,92 @@ +import Str from 'expensify-common/lib/str'; +import PropTypes from 'prop-types'; +import React, {useMemo, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import compose from '@libs/compose'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SelectionList from './SelectionList'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; + +const propTypes = { + /** Label for the search text input */ + textInputLabel: PropTypes.string.isRequired, + + /** Callback to fire when a currency is selected */ + onSelect: PropTypes.func.isRequired, + + /** Currency item to be selected initially */ + initiallySelectedCurrencyCode: PropTypes.string.isRequired, + + // The curency list constant object from Onyx + currencyList: PropTypes.objectOf( + PropTypes.shape({ + // Symbol for the currency + symbol: PropTypes.string, + + // Name of the currency + name: PropTypes.string, + + // ISO4217 Code for the currency + ISO4217: PropTypes.string, + }), + ), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + currencyList: {}, +}; + +function CurrencySelectionList(props) { + const [searchValue, setSearchValue] = useState(''); + const {translate, currencyList} = props; + + const {sections, headerMessage} = useMemo(() => { + const currencyOptions = _.map(currencyList, (currencyInfo, currencyCode) => { + const isSelectedCurrency = currencyCode === props.initiallySelectedCurrencyCode; + return { + currencyName: currencyInfo.name, + text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, + currencyCode, + keyForList: currencyCode, + isSelected: isSelectedCurrency, + }; + }); + + const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i'); + const filteredCurrencies = _.filter(currencyOptions, (currencyOption) => searchRegex.test(currencyOption.text) || searchRegex.test(currencyOption.currencyName)); + const isEmpty = searchValue.trim() && !filteredCurrencies.length; + + return { + sections: isEmpty ? [] : [{data: filteredCurrencies, indexOffset: 0}], + headerMessage: isEmpty ? translate('common.noResultsFound') : '', + }; + }, [currencyList, searchValue, translate, props.initiallySelectedCurrencyCode]); + + return ( + <SelectionList + sections={sections} + textInputLabel={props.textInputLabel} + textInputValue={searchValue} + onChangeText={setSearchValue} + onSelectRow={props.onSelect} + headerMessage={headerMessage} + initiallyFocusedOptionKey={props.initiallySelectedCurrencyCode} + showScrollIndicator + /> + ); +} + +CurrencySelectionList.displayName = 'CurrencySelectionList'; +CurrencySelectionList.propTypes = propTypes; +CurrencySelectionList.defaultProps = defaultProps; + +export default compose( + withLocalize, + withOnyx({ + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, + }), +)(CurrencySelectionList); diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index c7b5885865df..a18929061f1f 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -1,17 +1,15 @@ -import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {Keyboard} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import CurrencySelectionList from '@components/CurrencySelectionList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -42,20 +40,6 @@ const propTypes = { }), }).isRequired, - // The currency list constant object from Onyx - currencyList: PropTypes.objectOf( - PropTypes.shape({ - // Symbol for the currency - symbol: PropTypes.string, - - // Name of the currency - name: PropTypes.string, - - // ISO4217 Code for the currency - ISO4217: PropTypes.string, - }), - ), - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: iouPropTypes, @@ -63,13 +47,10 @@ const propTypes = { }; const defaultProps = { - currencyList: {}, iou: iouDefaultProps, }; function IOUCurrencySelection(props) { - const [searchValue, setSearchValue] = useState(''); - const optionsSelectorRef = useRef(); const selectedCurrencyCode = (lodashGet(props.route, 'params.currency', props.iou.currency) || CONST.CURRENCY.USD).toUpperCase(); const iouType = lodashGet(props.route, 'params.iouType', CONST.IOU.TYPE.REQUEST); const reportID = lodashGet(props.route, 'params.reportID', ''); @@ -113,59 +94,19 @@ function IOUCurrencySelection(props) { [props.route, props.navigation], ); - const {translate, currencyList} = props; - const {sections, headerMessage, initiallyFocusedOptionKey} = useMemo(() => { - const currencyOptions = _.map(currencyList, (currencyInfo, currencyCode) => { - const isSelectedCurrency = currencyCode === selectedCurrencyCode; - return { - currencyName: currencyInfo.name, - text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, - currencyCode, - keyForList: currencyCode, - isSelected: isSelectedCurrency, - }; - }); - - const searchRegex = new RegExp(Str.escapeForRegExp(searchValue.trim()), 'i'); - const filteredCurrencies = _.filter(currencyOptions, (currencyOption) => searchRegex.test(currencyOption.text) || searchRegex.test(currencyOption.currencyName)); - const isEmpty = searchValue.trim() && !filteredCurrencies.length; - - return { - initiallyFocusedOptionKey: _.get( - _.find(filteredCurrencies, (currency) => currency.currencyCode === selectedCurrencyCode), - 'keyForList', - ), - sections: isEmpty - ? [] - : [ - { - data: filteredCurrencies, - indexOffset: 0, - }, - ], - headerMessage: isEmpty ? translate('common.noResultsFound') : '', - }; - }, [currencyList, searchValue, selectedCurrencyCode, translate]); - return ( <ScreenWrapper includeSafeAreaPaddingBottom={false} - onEntryTransitionEnd={() => optionsSelectorRef.current && optionsSelectorRef.current.focus()} testID={IOUCurrencySelection.displayName} > <HeaderWithBackButton - title={translate('common.selectCurrency')} + title={props.translate('common.selectCurrency')} onBackButtonPress={() => Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID))} /> - <SelectionList - sections={sections} - textInputLabel={translate('common.search')} - textInputValue={searchValue} - onChangeText={setSearchValue} - onSelectRow={confirmCurrencySelection} - headerMessage={headerMessage} - initiallyFocusedOptionKey={initiallyFocusedOptionKey} - showScrollIndicator + <CurrencySelectionList + textInputLabel={props.translate('common.search')} + onSelect={confirmCurrencySelection} + initiallySelectedCurrencyCode={selectedCurrencyCode} /> </ScreenWrapper> ); @@ -178,7 +119,6 @@ IOUCurrencySelection.defaultProps = defaultProps; export default compose( withLocalize, withOnyx({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, iou: {key: ONYXKEYS.IOU}, }), withNetwork(), diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index ce1e1d7b8966..a20e8e272e4e 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -1,73 +1,30 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import React, {useCallback} from 'react'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import CurrencySelectionList from '@components/CurrencySelectionList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; const propTypes = { - /** Constant, list of available currencies */ - currencyList: PropTypes.objectOf( - PropTypes.shape({ - /** Symbol of the currency */ - symbol: PropTypes.string.isRequired, - }), - ), isLoadingReportData: PropTypes.bool, ...policyPropTypes, }; const defaultProps = { - currencyList: {}, isLoadingReportData: true, ...policyDefaultProps, }; -const getDisplayText = (currencyCode, currencySymbol) => `${currencyCode} - ${currencySymbol}`; - -function WorkspaceSettingsCurrencyPage({currencyList, policy, isLoadingReportData}) { +function WorkspaceSettingsCurrencyPage({policy, isLoadingReportData}) { const {translate} = useLocalize(); - const [searchText, setSearchText] = useState(''); - const trimmedText = searchText.trim().toLowerCase(); - const currencyListKeys = _.keys(currencyList); - - const filteredItems = _.filter(currencyListKeys, (currencyCode) => { - const currency = currencyList[currencyCode]; - return getDisplayText(currencyCode, currency.symbol).toLowerCase().includes(trimmedText); - }); - - let initiallyFocusedOptionKey; - - const currencyItems = _.map(filteredItems, (currencyCode) => { - const currency = currencyList[currencyCode]; - const isSelected = policy.outputCurrency === currencyCode; - - if (isSelected) { - initiallyFocusedOptionKey = currencyCode; - } - - return { - text: getDisplayText(currencyCode, currency.symbol), - keyForList: currencyCode, - isSelected, - }; - }); - - const sections = [{data: currencyItems, indexOffset: 0}]; - - const headerMessage = searchText.trim() && !currencyItems.length ? translate('common.noResultsFound') : ''; - const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), [policy.id]); const onSelectCurrency = (item) => { @@ -90,15 +47,10 @@ function WorkspaceSettingsCurrencyPage({currencyList, policy, isLoadingReportDat onBackButtonPress={onBackButtonPress} /> - <SelectionList - sections={sections} + <CurrencySelectionList textInputLabel={translate('workspace.editor.currencyInputLabel')} - textInputValue={searchText} - onChangeText={setSearchText} - onSelectRow={onSelectCurrency} - headerMessage={headerMessage} - initiallyFocusedOptionKey={initiallyFocusedOptionKey} - showScrollIndicator + onSelect={onSelectCurrency} + initiallySelectedCurrencyCode={policy.outputCurrency} /> </FullPageNotFoundView> </ScreenWrapper> @@ -109,9 +61,4 @@ WorkspaceSettingsCurrencyPage.displayName = 'WorkspaceSettingsCurrencyPage'; WorkspaceSettingsCurrencyPage.propTypes = propTypes; WorkspaceSettingsCurrencyPage.defaultProps = defaultProps; -export default compose( - withPolicyAndFullscreenLoading, - withOnyx({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, - }), -)(WorkspaceSettingsCurrencyPage); +export default withPolicyAndFullscreenLoading(WorkspaceSettingsCurrencyPage); From 87c644c9f65a5d9845a92aa0a6531ace1855ddb4 Mon Sep 17 00:00:00 2001 From: Tsaqif <tsaiinkwa@yahoo.com> Date: Wed, 8 Nov 2023 20:19:09 +0700 Subject: [PATCH 002/580] Add currency url parameter for WorkspaceSettingsCurrencyPage Signed-off-by: Tsaqif <tsaiinkwa@yahoo.com> --- src/ROUTES.ts | 2 +- src/components/CurrencySelectionList.js | 5 ++++- src/pages/iou/IOUCurrencySelection.js | 1 + .../workspace/WorkspaceSettingsCurrencyPage.js | 18 +++++++++++++++++- src/pages/workspace/WorkspaceSettingsPage.js | 2 +- 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bcc4685368cb..0902309514fc 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -328,7 +328,7 @@ export default { }, WORKSPACE_SETTINGS_CURRENCY: { route: 'workspace/:policyID/settings/currency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/currency`, + getRoute: (policyID: string, currency: string) => `workspace/${policyID}/settings/currency?currency=${currency}`, }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', diff --git a/src/components/CurrencySelectionList.js b/src/components/CurrencySelectionList.js index 795e8477a523..ba3cc9f7d7ed 100644 --- a/src/components/CurrencySelectionList.js +++ b/src/components/CurrencySelectionList.js @@ -20,6 +20,9 @@ const propTypes = { /** Currency item to be selected initially */ initiallySelectedCurrencyCode: PropTypes.string.isRequired, + /** Currency item to be focused initially */ + initiallyFocusedCurrencyCode: PropTypes.string.isRequired, + // The curency list constant object from Onyx currencyList: PropTypes.objectOf( PropTypes.shape({ @@ -79,7 +82,7 @@ function CurrencySelectionList(props) { onChangeText={setSearchValue} onSelectRow={props.onSelect} headerMessage={headerMessage} - initiallyFocusedOptionKey={props.initiallySelectedCurrencyCode} + initiallyFocusedOptionKey={props.initiallyFocusedCurrencyCode} showScrollIndicator /> ); diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index a18929061f1f..3fce092abda0 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -106,6 +106,7 @@ function IOUCurrencySelection(props) { <CurrencySelectionList textInputLabel={props.translate('common.search')} onSelect={confirmCurrencySelection} + initiallyFocusedCurrencyCode={selectedCurrencyCode} initiallySelectedCurrencyCode={selectedCurrencyCode} /> </ScreenWrapper> diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index a20e8e272e4e..2bb2a283acbc 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -1,3 +1,4 @@ +import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; import _ from 'underscore'; @@ -6,6 +7,7 @@ import CurrencySelectionList from '@components/CurrencySelectionList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; @@ -19,12 +21,25 @@ const propTypes = { }; const defaultProps = { + /** Route from navigation */ + route: PropTypes.shape({ + /** Params from the route */ + params: PropTypes.shape({ + /** Focused currency code */ + currency: PropTypes.string, + + /** ID of a policy */ + policyID: PropTypes.string, + }), + }).isRequired, isLoadingReportData: true, ...policyDefaultProps, }; -function WorkspaceSettingsCurrencyPage({policy, isLoadingReportData}) { +function WorkspaceSettingsCurrencyPage({route, policy, isLoadingReportData}) { const {translate} = useLocalize(); + const currencyParam = lodashGet(route, 'params.currency', '').toUpperCase(); + const focusedCurrencyCode = CurrencyUtils.isValidCurrencyCode(currencyParam) ? currencyParam : policy.outputCurrency; const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), [policy.id]); const onSelectCurrency = (item) => { @@ -50,6 +65,7 @@ function WorkspaceSettingsCurrencyPage({policy, isLoadingReportData}) { <CurrencySelectionList textInputLabel={translate('workspace.editor.currencyInputLabel')} onSelect={onSelectCurrency} + initiallyFocusedCurrencyCode={focusedCurrencyCode} initiallySelectedCurrencyCode={policy.outputCurrency} /> </FullPageNotFoundView> diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index d913ae26c170..287f31305164 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -87,7 +87,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { return errors; }, []); - const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id)), [policy.id]); + const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id, policy.outputCurrency)), [policy.id, policy.outputCurrency]); const policyName = lodashGet(policy, 'name', ''); From 1e508c14374e7d0809770031c7fd455eb593edf9 Mon Sep 17 00:00:00 2001 From: Tsaqif <tsaiinkwa@yahoo.com> Date: Fri, 24 Nov 2023 07:45:11 +0700 Subject: [PATCH 003/580] Don't automatically save ws default currency when selecting currency in selection list Signed-off-by: Tsaqif <tsaiinkwa@yahoo.com> --- src/ROUTES.ts | 2 +- src/components/CurrencySelectionList.js | 5 +---- .../workspace/WorkspaceSettingsCurrencyPage.js | 11 ++++------- src/pages/workspace/WorkspaceSettingsPage.js | 16 +++++++++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0902309514fc..9545a7ab5008 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -324,7 +324,7 @@ export default { }, WORKSPACE_SETTINGS: { route: 'workspace/:policyID/settings', - getRoute: (policyID: string) => `workspace/${policyID}/settings`, + getRoute: (policyID: string, currency: string) => `workspace/${policyID}/settings?currency=${currency}`, }, WORKSPACE_SETTINGS_CURRENCY: { route: 'workspace/:policyID/settings/currency', diff --git a/src/components/CurrencySelectionList.js b/src/components/CurrencySelectionList.js index ba3cc9f7d7ed..795e8477a523 100644 --- a/src/components/CurrencySelectionList.js +++ b/src/components/CurrencySelectionList.js @@ -20,9 +20,6 @@ const propTypes = { /** Currency item to be selected initially */ initiallySelectedCurrencyCode: PropTypes.string.isRequired, - /** Currency item to be focused initially */ - initiallyFocusedCurrencyCode: PropTypes.string.isRequired, - // The curency list constant object from Onyx currencyList: PropTypes.objectOf( PropTypes.shape({ @@ -82,7 +79,7 @@ function CurrencySelectionList(props) { onChangeText={setSearchValue} onSelectRow={props.onSelect} headerMessage={headerMessage} - initiallyFocusedOptionKey={props.initiallyFocusedCurrencyCode} + initiallyFocusedOptionKey={props.initiallySelectedCurrencyCode} showScrollIndicator /> ); diff --git a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js index 2bb2a283acbc..b86ee9e34404 100644 --- a/src/pages/workspace/WorkspaceSettingsCurrencyPage.js +++ b/src/pages/workspace/WorkspaceSettingsCurrencyPage.js @@ -10,7 +10,6 @@ import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; -import * as Policy from '@userActions/Policy'; import ROUTES from '@src/ROUTES'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -39,12 +38,11 @@ const defaultProps = { function WorkspaceSettingsCurrencyPage({route, policy, isLoadingReportData}) { const {translate} = useLocalize(); const currencyParam = lodashGet(route, 'params.currency', '').toUpperCase(); - const focusedCurrencyCode = CurrencyUtils.isValidCurrencyCode(currencyParam) ? currencyParam : policy.outputCurrency; - const onBackButtonPress = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)), [policy.id]); + const selectedCurrencyCode = CurrencyUtils.isValidCurrencyCode(currencyParam) ? currencyParam : policy.outputCurrency; + const onBackButtonPress = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id, selectedCurrencyCode)), [policy.id, selectedCurrencyCode]); const onSelectCurrency = (item) => { - Policy.updateGeneralSettings(policy.id, policy.name, item.keyForList); - Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)); + Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id, item.currencyCode)); }; return ( @@ -65,8 +63,7 @@ function WorkspaceSettingsCurrencyPage({route, policy, isLoadingReportData}) { <CurrencySelectionList textInputLabel={translate('workspace.editor.currencyInputLabel')} onSelect={onSelectCurrency} - initiallyFocusedCurrencyCode={focusedCurrencyCode} - initiallySelectedCurrencyCode={policy.outputCurrency} + initiallySelectedCurrencyCode={selectedCurrencyCode} /> </FullPageNotFoundView> </ScreenWrapper> diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 287f31305164..df43145d6ffd 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -16,6 +16,7 @@ import TextInput from '@components/TextInput'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; 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 * as UserUtils from '@libs/UserUtils'; @@ -42,6 +43,9 @@ const propTypes = { params: PropTypes.shape({ /** The policyID that is being configured */ policyID: PropTypes.string.isRequired, + + /** Selected currency code */ + currency: PropTypes.string, }).isRequired, }).isRequired, @@ -56,8 +60,10 @@ const defaultProps = { function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { const {translate} = useLocalize(); + const currencyParam = lodashGet(route, 'params.currency', '').toUpperCase(); + const currencyCode = CurrencyUtils.isValidCurrencyCode(currencyParam) ? currencyParam : policy.outputCurrency; - const formattedCurrency = !_.isEmpty(policy) && !_.isEmpty(currencyList) ? `${policy.outputCurrency} - ${currencyList[policy.outputCurrency].symbol}` : ''; + const formattedCurrency = !_.isEmpty(policy) && !_.isEmpty(currencyList) ? `${currencyCode} - ${currencyList[currencyCode].symbol}` : ''; const submit = useCallback( (values) => { @@ -65,11 +71,11 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { return; } - Policy.updateGeneralSettings(policy.id, values.name.trim(), policy.outputCurrency); + Policy.updateGeneralSettings(policy.id, values.name.trim(), currencyCode); Keyboard.dismiss(); - Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)); + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)); }, - [policy.id, policy.isPolicyUpdating, policy.outputCurrency], + [policy.id, policy.isPolicyUpdating, currencyCode], ); const validate = useCallback((values) => { @@ -87,7 +93,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { return errors; }, []); - const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id, policy.outputCurrency)), [policy.id, policy.outputCurrency]); + const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS_CURRENCY.getRoute(policy.id, currencyCode)), [policy.id, currencyCode]); const policyName = lodashGet(policy, 'name', ''); From 81dc4ec8a8ef6feb66e8e685857ad738b1eab33f Mon Sep 17 00:00:00 2001 From: Tsaqif <tsaiinkwa@yahoo.com> Date: Fri, 24 Nov 2023 09:43:02 +0700 Subject: [PATCH 004/580] Fix adding currency parameter to worksapace_settings get route calls Signed-off-by: Tsaqif <tsaiinkwa@yahoo.com> --- src/pages/workspace/WorkspaceInitialPage.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 77e831e62b63..1e27c89b405c 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -53,10 +53,10 @@ const defaultProps = { }; /** - * @param {string} policyID + * @param {Object} policy */ -function openEditor(policyID) { - Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policyID)); +function openEditor(policy) { + Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id, policy.outputCurrency)); } /** @@ -152,7 +152,7 @@ function WorkspaceInitialPage(props) { { translationKey: 'workspace.common.settings', icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id)))), + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_SETTINGS.getRoute(policy.id, policy.outputCurrency)))), brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }, { @@ -269,7 +269,7 @@ function WorkspaceInitialPage(props) { <PressableWithoutFeedback disabled={hasPolicyCreationError || isExecuting} style={[styles.pRelative, styles.avatarLarge]} - onPress={singleExecution(waitForNavigate(() => openEditor(policy.id)))} + onPress={singleExecution(waitForNavigate(() => openEditor(policy)))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -289,7 +289,7 @@ function WorkspaceInitialPage(props) { <PressableWithoutFeedback disabled={hasPolicyCreationError || isExecuting} style={[styles.alignSelfCenter, styles.mt4, styles.w100]} - onPress={singleExecution(waitForNavigate(() => openEditor(policy.id)))} + onPress={singleExecution(waitForNavigate(() => openEditor(policy)))} accessibilityLabel={translate('workspace.common.settings')} role={CONST.ACCESSIBILITY_ROLE.BUTTON} > From 6c4c5b0cc041e57c020cf1c0efaa5976cd0f5e78 Mon Sep 17 00:00:00 2001 From: Tsaqif <tsaiinkwa@yahoo.com> Date: Sun, 3 Dec 2023 15:54:49 +0700 Subject: [PATCH 005/580] Change display text format of currency selection list Signed-off-by: Tsaqif <tsaiinkwa@yahoo.com> --- src/components/CurrencySelectionList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CurrencySelectionList.js b/src/components/CurrencySelectionList.js index 795e8477a523..7d43b09c8d66 100644 --- a/src/components/CurrencySelectionList.js +++ b/src/components/CurrencySelectionList.js @@ -50,7 +50,7 @@ function CurrencySelectionList(props) { const isSelectedCurrency = currencyCode === props.initiallySelectedCurrencyCode; return { currencyName: currencyInfo.name, - text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, + text: `${currencyCode} - ${CurrencyUtils.getCurrencySymbol(currencyCode)}`, currencyCode, keyForList: currencyCode, isSelected: isSelectedCurrency, From 06e5faa05332dd6969f949dcbb12bdc77b3cbe3f Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Wed, 31 Jan 2024 16:33:41 -0800 Subject: [PATCH 006/580] Create unlimited-virtual-cards.md This is a new help doc for the imminent release of Unlimited Virtual Cards. --- .../expensify-card/unlimited-virtual-cards.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md diff --git a/docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md b/docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md new file mode 100644 index 000000000000..c5578249289a --- /dev/null +++ b/docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md @@ -0,0 +1,86 @@ +--- +title: Unlimited Virtual Cards +description: Learn more about virtual cards and how they can help your business gain efficiency and insight into company spending. +--- + +# Overview + +For admins to issue virtual cards, your company **must upgrade to Expensify’s new Expensify Visa® Commercial Card.** + +Once upgraded to the new Expensify Card, admins can issue an unlimited number of virtual cards with a fixed or monthly limit for specific company purchases or recurring subscription payments _(e.g., Marketing purchases, Advertising, Travel, Amazon Web Services, etc.)._ + +This feature supports businesses that require tighter controls on company spending, allowing customers to set fixed or monthly spending limits for each virtual card. + +Use virtual cards if your company needs or wants: + +- To use one card per vendor or subscription, +- To issue cards for one-time purchases with a fixed amount, +- To issue cards for events or trips, +- To issue cards with a low limit that renews monthly, + +Admins can also name each virtual card, making it easy to categorize and assign them to specific accounts upon creation. Naming the card ensures a clear and organized overview of expenses within the Expensify platform. + +# How to set up virtual cards + +After adopting the new Expensify Card, domain admins can issue virtual cards to any employee using an email matching your domain. Once created and assigned, the card will be visible under the name given to the card. + +**To assign a virtual card:** + +1. Head to **Settings** > **Domains** > [**Company Cards**](https://www.expensify.com/domain_companycards). +2. Click the **Issue Virtual Cards** button. +3. Enter a card name (i.e., "Google Ads"). +4. Select a domain member to assign the card to. +5. Enter a card limit. +6. Select a **Limit Type** of _Fixed_ or _Monthly_. +7. Click **Issue Card**. + +![The Issue Virtual Cards modal is open in the middle of the screen. There are four options to set; Card Name, Assignee, Card Limit, and Limit type. A cancel (left) and save (right) button are at the bottom right of the modal.]({{site.url}}/assets/images/AdminissuedVirtualCards.png){:width="100%"} + +# How to edit virtual cards + +Domain admin can update the details of a virtual card on the [Company Cards](https://www.expensify.com/domain_companycards) page. + +**To edit a virtual card:** + +1. Click the **Edit** button to the right of the card. +2. Change the editable details. +3. Click **Edit Card** to save the changes. + +# How to terminate a virtual card + +Domain admin can also terminate a virtual card on the [Company Cards](https://www.expensify.com/domain_companycards) page by setting the limit to $0. + +**To terminate a virtual card:** + +1. Click the **Edit** button to the right of the card. +2. Set the limit to $0. +3. Click **Save**. +4. Refresh your web page, and the card will be removed from the list. + +{% include faq-begin.md %} + +**What is the difference between a fixed limit and a monthly limit?** + +There are two different limit types that are best suited for their intended purpose. + +- _Fixed limit_ spend cards are ideal for one-time expenses or providing employees access to a card for a designated purchase. +- _Monthly_ limit spend cards are perfect for managing recurring expenses such as subscriptions and memberships. + +**Where can employees see their virtual cards?** + +Employees can see their assigned virtual cards by navigating to **Settings** > **Account** > [**Credit Cards Import**](https://www.expensify.com/settings?param=%7B%22section%22:%22creditcards%22%7D) in their account. + +On this page, employees can see the remaining card limit, the type of card it is (i.e., fixed or monthly), and view the name given to the card. + +When the employee needs to use the card, they’ll click the **Show Details** button to expose the card details for making purchases. + +_Note: If the employee doesn’t have Two-Factor Authentication (2FA) enabled when they display the card details, they’ll be prompted to enable it. Enabling 2FA for their account provides the best protection from fraud and is **required** to dispute virtual card expenses._ + +**What do I do when there is fraud on one of our virtual cards?** + +If you or an employee loses their virtual card, experiences fraud, or suspects the card details are no longer secure, please [request a new card](https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-A-Transaction) immediately. A domain admin can also set the limit for the card to $0 to terminate the specific card immediately if the employee cannot take action. + +When the employee requests a new card, the compromised card will be terminated immediately. This is best practice for any Expensify Card and if fraud is suspected, action should be taken as soon as possible to reduce financial impact on the company. + +{% include faq-end.md %} + From 93072a93a9f792e0a63a5660b6bb8bbfb83429d6 Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Wed, 31 Jan 2024 16:37:45 -0800 Subject: [PATCH 007/580] Add files via upload --- docs/assets/images/AdminissuedVirtualCards.png | Bin 0 -> 157289 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/images/AdminissuedVirtualCards.png diff --git a/docs/assets/images/AdminissuedVirtualCards.png b/docs/assets/images/AdminissuedVirtualCards.png new file mode 100644 index 0000000000000000000000000000000000000000..88df9b2f3fecef011db3e71c7eb12c02847cc97b GIT binary patch literal 157289 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	k)YUf{LS?4%AzmB5_TZ?Gd5RHBr^RBHh&LNI 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�Wekz-QDtNuolrAzA6q~~*7 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 P=lAngc9vDAuig0{Q7&N7 literal 0 HcmV?d00001 From 1d4d9086293c1aec9bb7eb8194c4b06ed2b82155 Mon Sep 17 00:00:00 2001 From: dukenv0307 <dukenv0307@gmail.com> Date: Thu, 22 Feb 2024 00:58:36 +0700 Subject: [PATCH 008/580] fix: 32978 --- src/libs/SidebarUtils.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 49436576295c..9ac5227399f8 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -292,27 +292,35 @@ function getOptionData({ const isThreadMessage = ReportUtils.isThread(report) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && reportAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + // Currently the back-end is not returning the `lastActionName` so I use this to mock, + // ideally it should be returned from the back-end in the `report`, like the `lastMessageText` and `lastMessageHtml` + if (report.lastMessageHtml && report.lastMessageHtml.includes('invited <mention-user')) { + report.lastActionName = CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM; + } + if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport || isThreadMessage) && !result.isArchivedRoom) { const lastAction = visibleReportActionItems[report.reportID]; + const lastActionName = lastAction?.actionName ?? report.lastActionName; + if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { const newName = lastAction?.originalMessage?.newName ?? ''; result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName}); } else if (ReportActionsUtils.isTaskAction(lastAction)) { result.alternateText = TaskUtils.getTaskReportActionMessage(lastAction.actionName); } else if ( - lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || - lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM || - lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM || - lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.REMOVE_FROM_ROOM + lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || + lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM || + lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM || + lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.REMOVE_FROM_ROOM ) { - const targetAccountIDs = lastAction?.originalMessage?.targetAccountIDs ?? []; + const targetAccountIDsLength = lastAction?.originalMessage?.targetAccountIDs?.length ?? report.lastMessageHtml?.match(/<mention-user[^>]*><\/mention-user>/g)?.length ?? []; const verb = - lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM + lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM ? Localize.translate(preferredLocale, 'workspace.invite.invited') : Localize.translate(preferredLocale, 'workspace.invite.removed'); - const users = Localize.translate(preferredLocale, targetAccountIDs.length > 1 ? 'workspace.invite.users' : 'workspace.invite.user'); - result.alternateText = `${lastActorDisplayName} ${verb} ${targetAccountIDs.length} ${users}`.trim(); + const users = Localize.translate(preferredLocale, targetAccountIDsLength > 1 ? 'workspace.invite.users' : 'workspace.invite.user'); + result.alternateText = `${lastActorDisplayName} ${verb} ${targetAccountIDsLength} ${users}`.trim(); const roomName = lastAction?.originalMessage?.roomName ?? ''; if (roomName) { From 9467b3a7f068b071f380ae36a3e39aeeb607b1e1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Wed, 6 Mar 2024 13:35:59 +0530 Subject: [PATCH 009/580] feat: Receipt Audit Feature / Note type violations. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 33 +++++++++++++++++++ .../ReportActionItem/MoneyRequestView.tsx | 4 +++ src/styles/index.ts | 13 ++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/components/ReceiptAudit.tsx diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx new file mode 100644 index 000000000000..af8c063668e1 --- /dev/null +++ b/src/components/ReceiptAudit.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {View} from 'react-native'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import Text from './Text'; + +export default function ReceiptAudit({notes}: {notes: string[]}) { + const styles = useThemeStyles(); + const theme = useTheme(); + + return ( + <View style={[styles.mt2, styles.mb1, styles.ph5]}> + <View style={[styles.flexRow, styles.alignItemsCenter, styles.gap2]}> + <View style={[styles.receiptAuditTitleContainer, {backgroundColor: notes.length ? theme.danger : theme.success}]}> + <Icon + width={18} + height={18} + src={notes.length > 0 ? Expensicons.Receipt : Expensicons.Checkmark} + fill={theme.white} + /> + <Text style={[styles.textLabel, styles.textStrong, styles.textWhite]}> + {notes.length > 0 ? `Receipt Audit : ${notes.length} Issue(s) Found` : 'Receipt Verified : No issues Found'} + </Text> + </View> + </View> + + {/* // If notes is a array of strings, map through it & show notes. */} + <View style={[styles.mv1, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View> + </View> + ); +} diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 0bd18d8ee7ea..234aa4465d40 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -6,6 +6,7 @@ import ConfirmedRoute from '@components/ConfirmedRoute'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import ReceiptAudit from '@components/ReceiptAudit'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; import SpacerView from '@components/SpacerView'; import Switch from '@components/Switch'; @@ -285,6 +286,9 @@ function MoneyRequestView({ } /> )} + + <ReceiptAudit notes={['Amount greater than scanner total', 'Date differs from scanned date']} /> + {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription diff --git a/src/styles/index.ts b/src/styles/index.ts index 405a05cfce78..28f1d86f26cc 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4197,6 +4197,19 @@ const styles = (theme: ThemeColors) => borderWidth: 1, }, + receiptAuditTitleContainer: { + flexDirection: 'row', + gap: 4, + padding: 4, + paddingHorizontal: 8, + height: variables.inputHeightSmall, + borderRadius: variables.componentBorderRadiusSmall, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.border, + }, + mapViewContainer: { ...flex.flex1, minHeight: 300, From 8c400c3fae12d28f95ded76a52806e92ec54f740 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Wed, 6 Mar 2024 14:57:49 +0530 Subject: [PATCH 010/580] added translations. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 8 +++++--- src/components/ReportActionItem/MoneyRequestView.tsx | 5 ++++- src/languages/en.ts | 4 ++++ src/languages/es.ts | 4 ++++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index af8c063668e1..7f11e9b9a1e4 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Icon from './Icon'; @@ -9,7 +10,9 @@ import Text from './Text'; export default function ReceiptAudit({notes}: {notes: string[]}) { const styles = useThemeStyles(); const theme = useTheme(); + const {translate} = useLocalize(); + const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('iou.receiptNoIssuesFound'); return ( <View style={[styles.mt2, styles.mb1, styles.ph5]}> <View style={[styles.flexRow, styles.alignItemsCenter, styles.gap2]}> @@ -20,10 +23,9 @@ export default function ReceiptAudit({notes}: {notes: string[]}) { src={notes.length > 0 ? Expensicons.Receipt : Expensicons.Checkmark} fill={theme.white} /> - <Text style={[styles.textLabel, styles.textStrong, styles.textWhite]}> - {notes.length > 0 ? `Receipt Audit : ${notes.length} Issue(s) Found` : 'Receipt Verified : No issues Found'} - </Text> + <Text style={[styles.textLabel, styles.textStrong, styles.textWhite]}>{notes.length > 0 ? translate('iou.receiptAudit') : translate('iou.receiptVerified')}</Text> </View> + <Text style={[styles.textLabel, styles.textSupporting]}>{issuesFoundText}</Text> </View> {/* // If notes is a array of strings, map through it & show notes. */} diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 234aa4465d40..639739780a73 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -287,7 +287,10 @@ function MoneyRequestView({ /> )} - <ReceiptAudit notes={['Amount greater than scanner total', 'Date differs from scanned date']} /> + <ReceiptAudit + notes={['Amount greater than scanner total', 'Date differs from scanned date']} + // notes={[]} + /> {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> diff --git a/src/languages/en.ts b/src/languages/en.ts index 0a52cca62ef5..f073e79335ea 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -601,6 +601,10 @@ export default { posted: 'Posted', deleteReceipt: 'Delete receipt', routePending: 'Route pending...', + receiptAudit: 'Receipt Audit', + receiptVerified: 'Receipt Verified', + receiptNoIssuesFound: 'No issues Found', + receiptIssuesFound: (count: number) => `${count} Issue(s) Found`, receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', diff --git a/src/languages/es.ts b/src/languages/es.ts index 013255c1e11e..b908300f4e46 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -594,6 +594,10 @@ export default { posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', routePending: 'Ruta pendiente...', + receiptAudit: 'Auditoría de recibos', + receiptVerified: 'Recibo verificado', + receiptNoIssuesFound: 'No se encontraron problemas', + receiptIssuesFound: (count: number) => `Se encontró ${count} problema(s)`, receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', From 96744d5c4aa2622075e9a2febbe7d2c357eb740d Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Wed, 6 Mar 2024 15:56:41 +0530 Subject: [PATCH 011/580] extract noticeViolations from transactionViolations. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 6 +++--- src/components/ReportActionItem/MoneyRequestView.tsx | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 7f11e9b9a1e4..f756ecd5dc7e 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -7,14 +7,14 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; -export default function ReceiptAudit({notes}: {notes: string[]}) { +export default function ReceiptAudit({notes = []}: {notes?: string[]}) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('iou.receiptNoIssuesFound'); return ( - <View style={[styles.mt2, styles.mb1, styles.ph5]}> + <View style={[styles.mt1, styles.mb2, styles.ph5]}> <View style={[styles.flexRow, styles.alignItemsCenter, styles.gap2]}> <View style={[styles.receiptAuditTitleContainer, {backgroundColor: notes.length ? theme.danger : theme.success}]}> <Icon @@ -29,7 +29,7 @@ export default function ReceiptAudit({notes}: {notes: string[]}) { </View> {/* // If notes is a array of strings, map through it & show notes. */} - <View style={[styles.mv1, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View> + <View style={[styles.mt2, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View> </View> ); } diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 639739780a73..c4f067e35e89 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,6 +147,7 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); + const noticeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); let amountDescription = `${translate('iou.amount')}`; @@ -286,11 +287,7 @@ function MoneyRequestView({ } /> )} - - <ReceiptAudit - notes={['Amount greater than scanner total', 'Date differs from scanned date']} - // notes={[]} - /> + {noticeViolations?.length && <ReceiptAudit notes={noticeViolations} />} {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> From 952a4dac2dff9293d91ce53a58467042262bb531 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Thu, 7 Mar 2024 11:50:47 +0530 Subject: [PATCH 012/580] Update the dot separator sub-state for the request preview. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- .../MoneyRequestPreviewContent.tsx | 3 +++ .../ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/TransactionUtils.ts | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 8577c9fa5f97..fb524c6b5cb2 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -87,6 +87,7 @@ function MoneyRequestPreviewContent({ const hasReceipt = TransactionUtils.hasReceipt(transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '', transactionViolations); + const hasNoteTypeViolations = TransactionUtils.hasNoteTypeViolation(transaction?.transactionID ?? '', transactionViolations); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); const shouldShowRBR = hasViolations || hasFieldErrors; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); @@ -159,6 +160,8 @@ function MoneyRequestPreviewContent({ const isTooLong = violations.filter((v) => v.type === 'violation').length > 1 || violationMessage.length > 15; message += ` • ${isTooLong ? translate('violations.reviewRequired') : violationMessage}`; } + } else if (hasNoteTypeViolations && transaction && !ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { + message += ` • ${translate('violations.reviewRequired')}`; } else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { message += ` • ${translate('iou.approved')}`; } else if (iouReport?.isWaitingOnBankAccount) { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index c4f067e35e89..440f8afb73ee 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,7 +147,7 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); - const noticeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noticeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); let amountDescription = `${translate('iou.amount')}`; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8a98fe0f2cdc..5dea541b2af2 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -593,6 +593,17 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation')); } +/** + * Checks if any violations for the provided transaction are of type 'note' + */ +function hasNoteTypeViolation(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): boolean { + return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'note')); +} + +function getTransactionNoteViolations(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): TransactionViolation[] | null { + return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((violation: TransactionViolation) => violation.type === 'note') ?? null; +} + function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): TransactionViolation[] | null { return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; } @@ -638,6 +649,7 @@ export { getTagArrayFromName, getTagForDisplay, getTransactionViolations, + getTransactionNoteViolations, getLinkedTransaction, getAllReportTransactions, hasReceipt, @@ -663,6 +675,7 @@ export { waypointHasValidAddress, getRecentTransactions, hasViolation, + hasNoteTypeViolation, }; export type {TransactionChanges}; From 8438383448a735e0d5dd871e10ae0e27f85fd9f4 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Thu, 7 Mar 2024 11:51:50 +0530 Subject: [PATCH 013/580] Remove redundant code. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/libs/TransactionUtils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 5dea541b2af2..bc2fcaa39290 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -600,10 +600,6 @@ function hasNoteTypeViolation(transactionID: string, transactionViolations: Onyx return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'note')); } -function getTransactionNoteViolations(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): TransactionViolation[] | null { - return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((violation: TransactionViolation) => violation.type === 'note') ?? null; -} - function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): TransactionViolation[] | null { return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; } @@ -649,7 +645,6 @@ export { getTagArrayFromName, getTagForDisplay, getTransactionViolations, - getTransactionNoteViolations, getLinkedTransaction, getAllReportTransactions, hasReceipt, From 7f975195bf133d3ef94d5b1123a53b5fd171a457 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Thu, 7 Mar 2024 11:56:22 +0530 Subject: [PATCH 014/580] minor fix. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 440f8afb73ee..481bcb177569 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,7 +147,12 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); - const noticeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noticeViolations = [ + {name: 'missingComment', type: 'violation'}, + {name: 'modifiedDate', type: 'violation'}, + ] + ?.filter((violation) => violation.type === 'note') + .map((v) => ViolationsUtils.getViolationTranslation(v, translate)); let amountDescription = `${translate('iou.amount')}`; @@ -287,7 +292,7 @@ function MoneyRequestView({ } /> )} - {noticeViolations?.length && <ReceiptAudit notes={noticeViolations} />} + <ReceiptAudit notes={noticeViolations} /> {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> From cebc960dcf6e17bc04f2c0ed7158c91e2b112a23 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Thu, 7 Mar 2024 12:20:13 +0530 Subject: [PATCH 015/580] minor fix. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 481bcb177569..79639a62babe 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,12 +147,7 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); - const noticeViolations = [ - {name: 'missingComment', type: 'violation'}, - {name: 'modifiedDate', type: 'violation'}, - ] - ?.filter((violation) => violation.type === 'note') - .map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); let amountDescription = `${translate('iou.amount')}`; @@ -292,8 +287,7 @@ function MoneyRequestView({ } /> )} - <ReceiptAudit notes={noticeViolations} /> - + <ReceiptAudit notes={noteTypeViolations} /> {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription From 86014ab56158310228a9f5cd20d21ae9bb593002 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Thu, 7 Mar 2024 15:25:49 +0530 Subject: [PATCH 016/580] hide ReceiptAudit when scan is in progress. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 79639a62babe..415a2666512a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -129,6 +129,7 @@ function MoneyRequestView({ const canEditDate = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); + const isReceiptBeingScanned = TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction); // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat @@ -287,7 +288,7 @@ function MoneyRequestView({ } /> )} - <ReceiptAudit notes={noteTypeViolations} /> + {!isReceiptBeingScanned && canUseViolations && <ReceiptAudit notes={noteTypeViolations} />} {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription From 3db41da8dcaf188939e53f342650ff6f24250a24 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Fri, 8 Mar 2024 16:53:21 +0530 Subject: [PATCH 017/580] minor updates. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 2 -- src/components/ReportActionItem/MoneyRequestView.tsx | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index f756ecd5dc7e..5b91fe97ca0d 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -27,8 +27,6 @@ export default function ReceiptAudit({notes = []}: {notes?: string[]}) { </View> <Text style={[styles.textLabel, styles.textSupporting]}>{issuesFoundText}</Text> </View> - - {/* // If notes is a array of strings, map through it & show notes. */} <View style={[styles.mt2, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View> </View> ); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 415a2666512a..ffe373aade00 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -129,7 +129,8 @@ function MoneyRequestView({ const canEditDate = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); - const isReceiptBeingScanned = TransactionUtils.hasReceipt(transaction) && !TransactionUtils.isReceiptBeingScanned(transaction); + const hasReceipt = TransactionUtils.hasReceipt(transaction); + const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat @@ -149,6 +150,7 @@ function MoneyRequestView({ const {getViolationsForField} = useViolations(transactionViolations ?? []); const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && hasReceipt; let amountDescription = `${translate('iou.amount')}`; @@ -190,7 +192,6 @@ function MoneyRequestView({ } } - const hasReceipt = TransactionUtils.hasReceipt(transaction); let receiptURIs; const hasErrors = canEdit && TransactionUtils.hasMissingSmartscanFields(transaction); if (hasReceipt) { @@ -288,7 +289,7 @@ function MoneyRequestView({ } /> )} - {!isReceiptBeingScanned && canUseViolations && <ReceiptAudit notes={noteTypeViolations} />} + {shouldShowNotesViolations && <ReceiptAudit notes={noteTypeViolations} />} {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription From b53833171f6cf42c67998fa757a464a9e65cb97e Mon Sep 17 00:00:00 2001 From: Krishna <belivethatkg@gmail.com> Date: Mon, 11 Mar 2024 11:50:00 +0530 Subject: [PATCH 018/580] Update MoneyRequestView.tsx --- 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 3569791d64d4..d79ff88a4f1e 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -147,7 +147,7 @@ function MoneyRequestView({ const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true)); const {getViolationsForField} = useViolations(transactionViolations ?? []); - const hasViolations = useCallback( + const hasViolations = useCallback( (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, [canUseViolations, getViolationsForField], ); From 48d1543f0ff3a75aea75eb2bb9f176671475c5dd Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Tue, 12 Mar 2024 13:12:52 +0530 Subject: [PATCH 019/580] fix: translations. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 2c4f5d21f66f..7979981392fe 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -604,7 +604,7 @@ export default { receiptAudit: 'Receipt Audit', receiptVerified: 'Receipt Verified', receiptNoIssuesFound: 'No issues Found', - receiptIssuesFound: (count: number) => `${count} Issue(s) Found`, + receiptIssuesFound: (count: number) => `${count} ${count === 1 ? 'Issue' : 'Issues'} Found`, receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2f6a064a3615..730d110a60a9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -597,7 +597,7 @@ export default { receiptAudit: 'Auditoría de recibos', receiptVerified: 'Recibo verificado', receiptNoIssuesFound: 'No se encontraron problemas', - receiptIssuesFound: (count: number) => `Se encontró ${count} problema(s)`, + receiptIssuesFound: (count: number) => `Se encontró ${count} ${count === 1 ? 'problema' : 'problemas'}`, receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', From 77165e73b114711e721e98d4b1c0ba6d74cd37d3 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Sun, 17 Mar 2024 18:23:23 +0530 Subject: [PATCH 020/580] minor fixes. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/languages/en.ts | 4 ++-- src/libs/TransactionUtils.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d79ff88a4f1e..096ac8b69863 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -151,7 +151,7 @@ function MoneyRequestView({ (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, [canUseViolations, getViolationsForField], ); - const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'note').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && hasReceipt; let amountDescription = `${translate('iou.amount')}`; diff --git a/src/languages/en.ts b/src/languages/en.ts index 7979981392fe..cdefb85e0168 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -603,8 +603,8 @@ export default { routePending: 'Route pending...', receiptAudit: 'Receipt Audit', receiptVerified: 'Receipt Verified', - receiptNoIssuesFound: 'No issues Found', - receiptIssuesFound: (count: number) => `${count} ${count === 1 ? 'Issue' : 'Issues'} Found`, + receiptNoIssuesFound: 'No issues found', + receiptIssuesFound: (count: number) => `${count} ${count === 1 ? 'issue' : 'issues'} found`, receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index bc2fcaa39290..86285aef06b3 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -597,7 +597,7 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti * Checks if any violations for the provided transaction are of type 'note' */ function hasNoteTypeViolation(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): boolean { - return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'note')); + return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'notice')); } function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): TransactionViolation[] | null { From 7635de8bf5fb4a5d8e17a122cddc4f610787960e Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Mon, 18 Mar 2024 13:14:44 +0100 Subject: [PATCH 021/580] Fix: Hold request style and visibility --- src/components/MoneyRequestHeader.tsx | 12 +++++++++--- src/components/MoneyRequestHeaderStatusBar.tsx | 10 ++++++++-- src/styles/index.ts | 5 ++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index a9304b9c3138..0d1425405a92 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -18,7 +18,6 @@ import type {Policy, Report, ReportAction, ReportActions, Session, Transaction} import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; -import HoldBanner from './HoldBanner'; import * as Expensicons from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import {usePersonalDetails} from './OnyxProvider'; @@ -119,7 +118,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, onSelected: () => changeMoneyRequestStatus(), }); } - if (!isOnHold && (isRequestIOU || canModifyStatus)) { + if (!isOnHold && (isRequestIOU || canModifyStatus) && !isScanning) { threeDotsMenuItems.push({ icon: Expensicons.Stopwatch, text: translate('iou.holdRequest'), @@ -188,10 +187,17 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, <MoneyRequestHeaderStatusBar title={translate('iou.receiptStatusTitle')} description={translate('iou.receiptStatusText')} + shouldShowBorderBottom={!isOnHold} + /> + )} + {isOnHold && ( + <MoneyRequestHeaderStatusBar + title={translate('iou.hold')} + description={translate('iou.requestOnHold')} shouldShowBorderBottom + badgeColorStyle={styles.badgeDanger} /> )} - {isOnHold && <HoldBanner />} </View> <ConfirmModal title={translate('iou.deleteRequest')} diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index 59ef4ee0bd26..99913c3b927b 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import Text from './Text'; @@ -12,14 +13,19 @@ type MoneyRequestHeaderStatusBarProps = { /** Whether we show the border bottom */ shouldShowBorderBottom: boolean; + + /** Badge background color Style */ + badgeColorStyle?: StyleProp<ViewStyle>; }; -function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom}: MoneyRequestHeaderStatusBarProps) { +function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom, badgeColorStyle}: MoneyRequestHeaderStatusBarProps) { const styles = useThemeStyles(); const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; + const backgroundColorStyle = badgeColorStyle ?? styles.moneyRequestHeaderStatusBarBadgeBackground; + return ( <View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.flexGrow1, styles.overflowHidden, styles.ph5, styles.pb3, borderBottomStyle]}> - <View style={[styles.moneyRequestHeaderStatusBarBadge]}> + <View style={[styles.moneyRequestHeaderStatusBarBadge, backgroundColorStyle]}> <Text style={[styles.textStrong, styles.textMicroBold]}>{title}</Text> </View> <View style={[styles.flexShrink1]}> diff --git a/src/styles/index.ts b/src/styles/index.ts index df89cd823fa4..ca7e4ae8f7cf 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4160,10 +4160,13 @@ const styles = (theme: ThemeColors) => display: 'flex', justifyContent: 'center', alignItems: 'center', - backgroundColor: theme.border, marginRight: 12, }, + moneyRequestHeaderStatusBarBadgeBackground: { + backgroundColor: theme.border, + }, + staticHeaderImage: { minHeight: 240, }, From 3e6c006ca8bc275f94b3cc8db694440fd4d1b76b Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Mon, 18 Mar 2024 15:50:35 +0100 Subject: [PATCH 022/580] Fix: scan status badge danger text --- src/components/MoneyRequestHeaderStatusBar.tsx | 7 ++++--- src/styles/index.ts | 8 ++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index 99913c3b927b..9bbee03b0926 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -21,12 +21,13 @@ type MoneyRequestHeaderStatusBarProps = { function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom, badgeColorStyle}: MoneyRequestHeaderStatusBarProps) { const styles = useThemeStyles(); const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; - const backgroundColorStyle = badgeColorStyle ?? styles.moneyRequestHeaderStatusBarBadgeBackground; + const badgeBackgroundColorStyle = badgeColorStyle ?? styles.moneyRequestHeaderStatusBarBadgeBackground; + const badgeTextColorStyle = badgeColorStyle ? styles.textMicroBoldDangerColor : styles.textMicroBoldColor; return ( <View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.flexGrow1, styles.overflowHidden, styles.ph5, styles.pb3, borderBottomStyle]}> - <View style={[styles.moneyRequestHeaderStatusBarBadge, backgroundColorStyle]}> - <Text style={[styles.textStrong, styles.textMicroBold]}>{title}</Text> + <View style={[styles.moneyRequestHeaderStatusBarBadge, badgeBackgroundColorStyle]}> + <Text style={[styles.textStrong, styles.textMicroBold, badgeTextColorStyle]}>{title}</Text> </View> <View style={[styles.flexShrink1]}> <Text style={[styles.textLabelSupporting]}>{description}</Text> diff --git a/src/styles/index.ts b/src/styles/index.ts index ca7e4ae8f7cf..306ab55dd215 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -383,6 +383,14 @@ const styles = (theme: ThemeColors) => lineHeight: variables.lineHeightSmall, }, + textMicroBoldColor: { + color: theme.text, + }, + + textMicroBoldDangerColor: { + color: theme.textLight, + }, + textMicroSupporting: { color: theme.textSupporting, fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, From 25cc0fa936630e866bca9a470604632869890b2f Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Wed, 20 Mar 2024 15:35:07 +0530 Subject: [PATCH 023/580] receipt audit design changes. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 33 +++++++++++-------- .../ReportActionItem/MoneyRequestView.tsx | 5 +-- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 5b91fe97ca0d..0c387ff0ae79 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -7,27 +7,32 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; -export default function ReceiptAudit({notes = []}: {notes?: string[]}) { +function ReceiptAuditHeader({notes = []}: {notes?: string[]}) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('iou.receiptNoIssuesFound'); return ( - <View style={[styles.mt1, styles.mb2, styles.ph5]}> - <View style={[styles.flexRow, styles.alignItemsCenter, styles.gap2]}> - <View style={[styles.receiptAuditTitleContainer, {backgroundColor: notes.length ? theme.danger : theme.success}]}> - <Icon - width={18} - height={18} - src={notes.length > 0 ? Expensicons.Receipt : Expensicons.Checkmark} - fill={theme.white} - /> - <Text style={[styles.textLabel, styles.textStrong, styles.textWhite]}>{notes.length > 0 ? translate('iou.receiptAudit') : translate('iou.receiptVerified')}</Text> - </View> - <Text style={[styles.textLabel, styles.textSupporting]}>{issuesFoundText}</Text> + <View style={[styles.ph5]}> + <View style={[styles.flexRow, styles.alignItemsCenter]}> + <Text style={[styles.textLabelSupporting]}>{translate('common.receipt')}</Text> + <Text style={[styles.textLabelSupporting]}>{` • ${issuesFoundText}`}</Text> + <Icon + width={12} + height={12} + src={notes.length > 0 ? Expensicons.DotIndicator : Expensicons.Checkmark} + fill={notes.length ? theme.danger : theme.success} + additionalStyles={styles.ml2} + /> </View> - <View style={[styles.mt2, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View> </View> ); } + +function ReceiptAuditMessages({notes = []}: {notes?: string[]}) { + const styles = useThemeStyles(); + return <View style={[styles.mt1, styles.mb2, styles.ph5, {gap: 6}]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View>; +} + +export {ReceiptAuditHeader, ReceiptAuditMessages}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 096ac8b69863..47a193946083 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -6,7 +6,7 @@ import ConfirmedRoute from '@components/ConfirmedRoute'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ReceiptAudit from '@components/ReceiptAudit'; +import {ReceiptAuditHeader, ReceiptAuditMessages} from '@components/ReceiptAudit'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; import SpacerView from '@components/SpacerView'; import Switch from '@components/Switch'; @@ -244,6 +244,7 @@ function MoneyRequestView({ <View style={[StyleUtils.getReportWelcomeContainerStyle(isSmallScreenWidth)]}> <AnimatedEmptyStateBackground /> <View style={[StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth)]}> + {shouldShowNotesViolations && <ReceiptAuditHeader notes={noteTypeViolations} />} {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {(showMapAsImage || hasReceipt) && ( <OfflineWithFeedback @@ -289,7 +290,7 @@ function MoneyRequestView({ } /> )} - {shouldShowNotesViolations && <ReceiptAudit notes={noteTypeViolations} />} + {shouldShowNotesViolations && <ReceiptAuditMessages notes={noteTypeViolations} />} {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription From da3ae8cf625c5b27771eb4879dce2707a5ce6c4a Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Fri, 22 Mar 2024 07:02:41 +0530 Subject: [PATCH 024/580] Receipt audit design updates. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 29 +++++++++++-------- .../ReportActionItem/MoneyRequestView.tsx | 10 +++++-- src/languages/en.ts | 6 ++-- src/languages/es.ts | 6 ++-- src/styles/index.ts | 13 --------- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 0c387ff0ae79..3ec5e7f8e6de 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -7,24 +7,29 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; -function ReceiptAuditHeader({notes = []}: {notes?: string[]}) { +function ReceiptAuditHeader({notes = [], showAuditMessage = false}: {notes?: string[]; showAuditMessage?: boolean}) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('iou.receiptNoIssuesFound'); + const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('common.verified'); return ( - <View style={[styles.ph5]}> + <View style={[styles.ph5, styles.mbn1]}> <View style={[styles.flexRow, styles.alignItemsCenter]}> <Text style={[styles.textLabelSupporting]}>{translate('common.receipt')}</Text> - <Text style={[styles.textLabelSupporting]}>{` • ${issuesFoundText}`}</Text> - <Icon - width={12} - height={12} - src={notes.length > 0 ? Expensicons.DotIndicator : Expensicons.Checkmark} - fill={notes.length ? theme.danger : theme.success} - additionalStyles={styles.ml2} - /> + {showAuditMessage && ( + <> + <Text style={[styles.textLabelSupporting, styles.textLarge]}>{' • '}</Text> + <Text style={[styles.textLabelSupporting]}>{`${issuesFoundText}`}</Text> + <Icon + width={12} + height={12} + src={notes.length > 0 ? Expensicons.DotIndicator : Expensicons.Checkmark} + fill={notes.length ? theme.danger : theme.success} + additionalStyles={styles.ml1} + /> + </> + )} </View> </View> ); @@ -32,7 +37,7 @@ function ReceiptAuditHeader({notes = []}: {notes?: string[]}) { function ReceiptAuditMessages({notes = []}: {notes?: string[]}) { const styles = useThemeStyles(); - return <View style={[styles.mt1, styles.mb2, styles.ph5, {gap: 6}]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View>; + return <View style={[styles.mtn1, styles.mb2, styles.ph5, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View>; } export {ReceiptAuditHeader, ReceiptAuditMessages}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 47a193946083..7fd5fec6d2b9 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -244,8 +244,12 @@ function MoneyRequestView({ <View style={[StyleUtils.getReportWelcomeContainerStyle(isSmallScreenWidth)]}> <AnimatedEmptyStateBackground /> <View style={[StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth)]}> - {shouldShowNotesViolations && <ReceiptAuditHeader notes={noteTypeViolations} />} - {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} + {hasReceipt && ( + <ReceiptAuditHeader + notes={noteTypeViolations} + showAuditMessage={shouldShowNotesViolations} + /> + )} {(showMapAsImage || hasReceipt) && ( <OfflineWithFeedback pendingAction={pendingAction} @@ -291,7 +295,7 @@ function MoneyRequestView({ /> )} {shouldShowNotesViolations && <ReceiptAuditMessages notes={noteTypeViolations} />} - {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} + <ViolationMessages violations={getViolationsForField('receipt')} /> <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription title={formattedTransactionAmount ? formattedTransactionAmount.toString() : ''} diff --git a/src/languages/en.ts b/src/languages/en.ts index 4ce2a22c9c20..cd8605fce084 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -291,6 +291,7 @@ export default { nonBillable: 'Non-billable', tag: 'Tag', receipt: 'Receipt', + verified: `Verified`, replace: 'Replace', distance: 'Distance', mile: 'mile', @@ -602,10 +603,7 @@ export default { posted: 'Posted', deleteReceipt: 'Delete receipt', routePending: 'Route pending...', - receiptAudit: 'Receipt Audit', - receiptVerified: 'Receipt Verified', - receiptNoIssuesFound: 'No issues found', - receiptIssuesFound: (count: number) => `${count} ${count === 1 ? 'issue' : 'issues'} found`, + receiptIssuesFound: (count: number) => `${count === 1 ? 'Issue' : 'Issues'} found`, receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', missingAmount: 'Missing amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9097bf1b1370..9dbaa2eadd79 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -281,6 +281,7 @@ export default { nonBillable: 'No facturable', tag: 'Etiqueta', receipt: 'Recibo', + verified: `Verificado`, replace: 'Sustituir', distance: 'Distancia', mile: 'milla', @@ -595,10 +596,7 @@ export default { posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', routePending: 'Ruta pendiente...', - receiptAudit: 'Auditoría de recibos', - receiptVerified: 'Recibo verificado', - receiptNoIssuesFound: 'No se encontraron problemas', - receiptIssuesFound: (count: number) => `Se encontró ${count} ${count === 1 ? 'problema' : 'problemas'}`, + receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema' : 'Problemas'}`, receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', missingAmount: 'Falta importe', diff --git a/src/styles/index.ts b/src/styles/index.ts index f25b349cba60..8a91291a0c71 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4225,19 +4225,6 @@ const styles = (theme: ThemeColors) => borderWidth: 1, }, - receiptAuditTitleContainer: { - flexDirection: 'row', - gap: 4, - padding: 4, - paddingHorizontal: 8, - height: variables.inputHeightSmall, - borderRadius: variables.componentBorderRadiusSmall, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: theme.border, - }, - mapViewContainer: { ...flex.flex1, minHeight: 300, From fd457cd933ade344d19b0a17d40b6cd47d9214e3 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Tue, 26 Mar 2024 03:35:19 +0530 Subject: [PATCH 025/580] show notes violation for only admins and approvers in a paid policy. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- .../ReportActionItem/MoneyRequestView.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 7fd5fec6d2b9..f17f635f3260 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -41,6 +41,9 @@ import type {TransactionPendingFieldsKey} from '@src/types/onyx/Transaction'; import ReportActionItemImage from './ReportActionItemImage'; type MoneyRequestViewTransactionOnyxProps = { + /** Session info for the currently logged in user. */ + session: OnyxEntry<OnyxTypes.Session>; + /** The transaction associated with the transactionThread */ transaction: OnyxEntry<OnyxTypes.Transaction>; @@ -85,6 +88,7 @@ function MoneyRequestView({ policyTagList, policy, transactionViolations, + session, }: MoneyRequestViewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -131,6 +135,10 @@ function MoneyRequestView({ const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); + const isActionOwner = typeof parentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && parentReportAction.actorAccountID === session?.accountID; + const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; + // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); @@ -152,7 +160,7 @@ function MoneyRequestView({ [canUseViolations, getViolationsForField], ); const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); - const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && hasReceipt; + const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report) && (isActionOwner || isPolicyAdmin || isApprover); let amountDescription = `${translate('iou.amount')}`; @@ -487,5 +495,8 @@ export default withOnyx<MoneyRequestViewPropsWithoutTransaction, MoneyRequestVie return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; }, }, + session: { + key: ONYXKEYS.SESSION, + }, })(MoneyRequestView), ); From 0515ffbd7daa444bfd7f7f3d855d6446fb1c341d Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Tue, 26 Mar 2024 12:59:31 +0100 Subject: [PATCH 026/580] make wallet page cards grouped by domain --- src/ROUTES.ts | 4 +- src/languages/en.ts | 12 ++ src/languages/es.ts | 12 ++ src/libs/Navigation/types.ts | 7 +- .../settings/Wallet/ExpensifyCardPage.tsx | 158 ++++++++++++------ .../settings/Wallet/PaymentMethodList.tsx | 62 +++++-- src/styles/index.ts | 5 + 7 files changed, 184 insertions(+), 76 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c216d5ac288c..2f0f5851407a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -84,8 +84,8 @@ const ROUTES = { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { - route: 'settings/wallet/card/:domain', - getRoute: (domain: string) => `settings/wallet/card/${domain}` as const, + route: 'settings/wallet/card/:domain/:cardId', + getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}` as const, }, SETTINGS_REPORT_FRAUD: { route: 'settings/wallet/card/:domain/report-virtual-fraud', diff --git a/src/languages/en.ts b/src/languages/en.ts index c3ad6d82d6b2..d5b6d5107f3f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1029,6 +1029,18 @@ export default { cardPage: { expensifyCard: 'Expensify Card', availableSpend: 'Remaining limit', + smartLimit: { + name: 'Smart limit', + title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card, and the limit will reset as your submitted expenses are approved.`, + }, + fixedLimit: { + name: 'Fixed limit', + title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card, and then it will deactivate.`, + }, + monthlyLimit: { + name: 'Monthly limit', + title: (formattedLimit: string) => `You can spend up to ${formattedLimit} on this card per month. The limit will reset on the 1st day of each calendar month.`, + }, virtualCardNumber: 'Virtual card number', physicalCardNumber: 'Physical card number', getPhysicalCard: 'Get physical card', diff --git a/src/languages/es.ts b/src/languages/es.ts index 78b80adb16d4..d28b112a5471 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1028,6 +1028,18 @@ export default { cardPage: { expensifyCard: 'Tarjeta Expensify', availableSpend: 'Límite restante', + smartLimit: { + name: 'Smart limit', + title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta al mes. El límite se restablecerá el primer día del mes.`, + }, + fixedLimit: { + name: 'Fixed limit', + title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta, luego se desactivará.`, + }, + monthlyLimit: { + name: 'Monthly limit', + title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta y el límite se restablecerá a medida que se aprueben tus gastos.`, + }, virtualCardNumber: 'Número de la tarjeta virtual', physicalCardNumber: 'Número de la tarjeta física', getPhysicalCard: 'Obtener tarjeta física', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3f85aec3a560..ae74ac795f3c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -111,7 +111,12 @@ type SettingsNavigatorParamList = { }; [SCREENS.SETTINGS.WALLET.ROOT]: undefined; [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: undefined; - [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: undefined; + [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: { + /** domain passed via route /settings/wallet/card/:domain/:card */ + domain: string; + /** cardId passed via route /settings/wallet/card/:domain/:card */ + cardId: string; + }; [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: undefined; [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: undefined; [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: { diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 4c8b02eabdc6..80f134fc575a 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -3,6 +3,7 @@ import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import CardPreview from '@components/CardPreview'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; @@ -20,7 +21,7 @@ import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as GetPhysicalCardUtils from '@libs/GetPhysicalCardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {PublicScreensParamList} from '@libs/Navigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Card from '@userActions/Card'; import * as Link from '@userActions/Link'; @@ -31,7 +32,6 @@ import type SCREENS from '@src/SCREENS'; import type {GetPhysicalCardForm} from '@src/types/form'; import type {LoginList, Card as OnyxCard, PrivatePersonalDetails} from '@src/types/onyx'; import type {TCardDetails} from '@src/types/onyx/Card'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import RedDotCardSection from './RedDotCardSection'; import CardDetails from './WalletPage/CardDetails'; @@ -49,7 +49,7 @@ type ExpensifyCardPageOnyxProps = { loginList: OnyxEntry<LoginList>; }; -type ExpensifyCardPageProps = ExpensifyCardPageOnyxProps & StackScreenProps<PublicScreensParamList, typeof SCREENS.TRANSITION_BETWEEN_APPS>; +type ExpensifyCardPageProps = ExpensifyCardPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.WALLET.DOMAIN_CARD>; function ExpensifyCardPage({ cardList, @@ -57,45 +57,85 @@ function ExpensifyCardPage({ privatePersonalDetails, loginList, route: { - params: {domain = ''}, + params: {domain = '', cardId = ''}, }, }: ExpensifyCardPageProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const domainCards = useMemo(() => cardList && CardUtils.getDomainCards(cardList)[domain], [cardList, domain]); - const virtualCard = useMemo(() => domainCards?.find((card) => card.isVirtual), [domainCards]); - const physicalCard = useMemo(() => domainCards?.find((card) => !card.isVirtual), [domainCards]); + const isCardDomain = !cardList?.[cardId].isAdminIssuedVirtualCard; - const [isLoading, setIsLoading] = useState(false); const [isNotFound, setIsNotFound] = useState(false); - const [details, setDetails] = useState<TCardDetails>(); - const [cardDetailsError, setCardDetailsError] = useState(''); - + const cardsToShow = useMemo( + () => (isCardDomain ? CardUtils.getDomainCards(cardList)[domain].filter((card) => !card.isAdminIssuedVirtualCard) : [cardList?.[cardId]]), + [isCardDomain, cardList, cardId, domain], + ); useEffect(() => { - if (!cardList) { - return; - } - setIsNotFound(isEmptyObject(virtualCard) && isEmptyObject(physicalCard)); - }, [cardList, physicalCard, virtualCard]); + setIsNotFound(!cardsToShow); + }, [cardList, cardsToShow]); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- availableSpend can be 0 - const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard?.availableSpend || virtualCard?.availableSpend || 0); + const virtualCards = useMemo(() => cardsToShow.filter((card) => card.isVirtual), [cardsToShow]); + const physicalCards = useMemo(() => cardsToShow.filter((card) => !card.isVirtual), [cardsToShow]); + const [cardsDetails, setCardsDetails] = useState<Record<number, TCardDetails | null>>({}); + const [isCardDetailsLoading, setIsCardDetailsLoading] = useState<Record<number, boolean>>({}); + const [cardsDetailsErrors, setCardsDetailsErrors] = useState<Record<number, string>>({}); - const handleRevealDetails = () => { - setIsLoading(true); + const handleRevealDetails = (revealedCardId: number) => { + setIsCardDetailsLoading((prevState: Record<number, boolean>) => { + const newLoadingStates = {...prevState}; + newLoadingStates[revealedCardId] = true; + return newLoadingStates; + }); // We can't store the response in Onyx for security reasons. // That is why this action is handled manually and the response is stored in a local state // Hence eslint disable here. // eslint-disable-next-line rulesdir/no-thenable-actions-in-views - Card.revealVirtualCardDetails(virtualCard?.cardID ?? 0) + Card.revealVirtualCardDetails(revealedCardId) .then((value) => { - setDetails(value as TCardDetails); - setCardDetailsError(''); + setCardsDetails((prevState: Record<number, TCardDetails | null>) => { + const newCardsDetails = {...prevState}; + newCardsDetails[revealedCardId] = value as TCardDetails; + return newCardsDetails; + }); + setCardsDetailsErrors((prevState) => { + const newCardsDetailsErrors = {...prevState}; + newCardsDetailsErrors[revealedCardId] = ''; + return newCardsDetailsErrors; + }); + }) + .catch((error) => { + setCardsDetailsErrors((prevState) => { + const newCardsDetailsErrors = {...prevState}; + newCardsDetailsErrors[revealedCardId] = error; + return newCardsDetailsErrors; + }); }) - .catch(setCardDetailsError) - .finally(() => setIsLoading(false)); + .finally(() => + setIsCardDetailsLoading((prevState: Record<number, boolean>) => { + const newLoadingStates = {...prevState}; + newLoadingStates[revealedCardId] = false; + return newLoadingStates; + }), + ); + }; + + const hasDetectedDomainFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN); + const hasDetectedIndividualFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL); + + const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(cardsToShow?.[0]?.availableSpend); + const getLimitStrings = (limitType: ValueOf<typeof CONST.EXPENSIFY_CARD.LIMIT_TYPES>) => { + switch (limitType) { + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART: + return {limitName: translate('cardPage.smartLimit.name'), limitTitle: translate('cardPage.smartLimit.title', formattedAvailableSpendAmount)}; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY: + return {limitName: translate('cardPage.monthlyLimit.name'), limitTitle: translate('cardPage.monthlyLimit.title', formattedAvailableSpendAmount)}; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED: + return {limitName: translate('cardPage.fixedLimit.name'), limitTitle: translate('cardPage.fixedLimit.title', formattedAvailableSpendAmount)}; + default: + return {limitName: '', limitTitle: ''}; + } }; + const {limitName, limitTitle} = getLimitStrings(cardsToShow?.[0]?.limitType); const goToGetPhysicalCardFlow = () => { let updatedDraftValues = draftValues; @@ -109,9 +149,6 @@ function ExpensifyCardPage({ GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(updatedDraftValues)); }; - const hasDetectedDomainFraud = domainCards?.some((card) => card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN); - const hasDetectedIndividualFraud = domainCards?.some((card) => card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL); - if (isNotFound) { return <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET)} />; } @@ -165,13 +202,21 @@ function ExpensifyCardPage({ interactive={false} titleStyle={styles.newKansasLarge} /> - {!isEmptyObject(virtualCard) && ( + <MenuItemWithTopDescription + description={limitName} + title={limitTitle} + interactive={false} + titleStyle={styles.walletCardLimit} + numberOfLinesTitle={3} + /> + + {virtualCards.map((card) => ( <> - {details?.pan ? ( + {!!cardsDetails[card.cardID] && cardsDetails[card.cardID]?.pan ? ( <CardDetails - pan={details.pan} - expiration={CardUtils.formatCardExpiration(details.expiration)} - cvv={details.cvv} + pan={cardsDetails[card.cardID]?.pan} + expiration={CardUtils.formatCardExpiration(cardsDetails[card.cardID]?.expiration ?? '')} + cvv={cardsDetails[card.cardID]?.cvv} domain={domain} /> ) : ( @@ -186,14 +231,14 @@ function ExpensifyCardPage({ <Button medium text={translate('cardPage.cardDetails.revealDetails')} - onPress={handleRevealDetails} - isDisabled={isLoading || isOffline} - isLoading={isLoading} + onPress={() => handleRevealDetails(card.cardID)} + isDisabled={isCardDetailsLoading[card.cardID] || isOffline} + isLoading={isCardDetailsLoading[card.cardID]} /> } /> <DotIndicatorMessage - messages={cardDetailsError ? {error: cardDetailsError} : {}} + messages={cardsDetailsErrors[card.cardID] ? {error: cardsDetailsErrors[card.cardID]} : {}} type="error" style={[styles.ph5]} /> @@ -207,27 +252,30 @@ function ExpensifyCardPage({ onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain))} /> </> - )} - {physicalCard?.state === CONST.EXPENSIFY_CARD.STATE.OPEN && ( - <> - <MenuItemWithTopDescription - description={translate('cardPage.physicalCardNumber')} - title={CardUtils.maskCard(physicalCard?.lastFourPAN)} - interactive={false} - titleStyle={styles.walletCardNumber} - /> - <MenuItem - title={translate('reportCardLostOrDamaged.report')} - icon={Expensicons.Flag} - shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain))} - /> - </> + ))} + {physicalCards.map( + (card) => + card.state === CONST.EXPENSIFY_CARD.STATE.OPEN && ( + <> + <MenuItemWithTopDescription + description={translate('cardPage.physicalCardNumber')} + title={CardUtils.maskCard(card?.lastFourPAN)} + interactive={false} + titleStyle={styles.walletCardNumber} + /> + <MenuItem + title={translate('reportCardLostOrDamaged.report')} + icon={Expensicons.Flag} + shouldShowRightIcon + onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain))} + /> + </> + ), )} </> )} </ScrollView> - {physicalCard?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED && ( + {physicalCards?.some((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED) && ( <Button success large @@ -236,7 +284,7 @@ function ExpensifyCardPage({ text={translate('activateCardPage.activatePhysicalCard')} /> )} - {physicalCard?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED && ( + {physicalCards?.some((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED) && ( <Button success large diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 2bbc73355d68..ba1294bb212e 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -114,6 +114,7 @@ type PaymentMethodItem = PaymentMethod & { title?: string; description: string; onPress?: (e: GestureResponderEvent | KeyboardEvent | undefined) => void; + isGroupedCardDomain?: boolean; canDismissError?: boolean; disabled?: boolean; shouldShowRightIcon?: boolean; @@ -196,36 +197,61 @@ function PaymentMethodList({ const filteredPaymentMethods = useMemo(() => { if (shouldShowAssignedCards) { const assignedCards = Object.values(cardList ?? {}) - // Filter by physical, active cards associated with a domain - .filter((card) => !card.isVirtual && !!card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0)); - - const numberPhysicalExpensifyCards = assignedCards.filter((card) => CardUtils.isExpensifyCard(card.cardID)).length; - + // Filter by active cards associated with a domain + .filter((card) => !!card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0)); const assignedCardsSorted = lodashSortBy(assignedCards, (card) => !CardUtils.isExpensifyCard(card.cardID)); - return assignedCardsSorted.map((card) => { - const isExpensifyCard = CardUtils.isExpensifyCard(card.cardID); + const assignedCardsGrouped: PaymentMethodItem[] = []; + assignedCardsSorted.forEach((card) => { const icon = getBankIcon({bankName: card.bank as BankName, isCard: true, styles}); - // In the case a user has been assigned multiple physical Expensify Cards under one domain, display the Card with PAN - const expensifyCardDescription = numberPhysicalExpensifyCards > 1 ? CardUtils.getCardDescription(card.cardID) : translate('walletPage.expensifyCard'); - const cartTitle = card.lastFourPAN ? `${card.bank} - ${card.lastFourPAN}` : card.bank; - return { + if (!CardUtils.isExpensifyCard(card.cardID)) { + assignedCardsGrouped.push({ + key: card.cardID.toString(), + title: card.bank, + description: card.domainName, + shouldShowRightIcon: false, + interactive: false, + canDismissError: false, + errors: card.errors, + brickRoadIndicator: + card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL + ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR + : undefined, + ...icon, + }); + return; + } + + // The card should be grouped to a specific domain and such domain already exists in a assignedCardsGrouped + if (assignedCardsGrouped.some((item) => item.isGroupedCardDomain && item.description === card.domainName) && !card.isAdminIssuedVirtualCard) { + const domainGroupIndex = assignedCardsGrouped.findIndex((item) => item.isGroupedCardDomain && item.description === card.domainName); + assignedCardsGrouped[domainGroupIndex].errors = {...assignedCardsGrouped[domainGroupIndex].errors, ...card.errors}; + if (card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL) { + assignedCardsGrouped[domainGroupIndex].brickRoadIndicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + } + return; + } + + // The card shouldn't be grouped or it's domain group doesn't exist yet + assignedCardsGrouped.push({ key: card.cardID.toString(), - title: isExpensifyCard ? expensifyCardDescription : cartTitle, + title: card.bank, description: card.domainName, - onPress: isExpensifyCard ? () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName ?? '')) : () => {}, - shouldShowRightIcon: isExpensifyCard, - interactive: isExpensifyCard, - canDismissError: isExpensifyCard, + onPress: () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName ?? '', card.cardID.toString() ?? '')), + isGroupedCardDomain: !card.isAdminIssuedVirtualCard, + shouldShowRightIcon: true, + interactive: true, + canDismissError: true, errors: card.errors, brickRoadIndicator: card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, ...icon, - }; + }); }); + return assignedCardsGrouped; } const paymentCardList = fundList ?? {}; @@ -271,7 +297,7 @@ function PaymentMethodList({ }); return combinedPaymentMethods; - }, [shouldShowAssignedCards, fundList, bankAccountList, styles, filterType, isOffline, cardList, translate, actionPaymentMethodType, activePaymentMethodID, StyleUtils, onPress]); + }, [shouldShowAssignedCards, fundList, bankAccountList, styles, filterType, isOffline, cardList, actionPaymentMethodType, activePaymentMethodID, StyleUtils, onPress]); /** * Render placeholder when there are no payments methods diff --git a/src/styles/index.ts b/src/styles/index.ts index 120b848bd5a4..44665b4516a1 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4397,6 +4397,11 @@ const styles = (theme: ThemeColors) => paddingVertical: 4, }, + walletCardLimit: { + color: theme.text, + fontSize: variables.fontSizeNormal, + }, + walletCard: { borderRadius: variables.componentBorderRadiusLarge, position: 'relative', From 4db5bf9a5a8b502230781084bf0e43a6ae4e8870 Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Thu, 28 Mar 2024 15:19:19 +0100 Subject: [PATCH 027/580] refactor --- src/components/MoneyReportHeaderStatusBar.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 2 +- src/components/MoneyRequestHeaderStatusBar.tsx | 11 +++++------ src/styles/index.ts | 4 ++++ 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx index 7d2b749cce0a..af23a63b21d0 100644 --- a/src/components/MoneyReportHeaderStatusBar.tsx +++ b/src/components/MoneyReportHeaderStatusBar.tsx @@ -24,7 +24,7 @@ function MoneyReportHeaderStatusBar({nextStep}: MoneyReportHeaderStatusBarProps) return ( <View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.overflowHidden, styles.w100]}> - <View style={styles.moneyRequestHeaderStatusBarBadge}> + <View style={[styles.moneyRequestHeaderStatusBarBadge, styles.moneyRequestHeaderStatusBarBadgeBackground]}> <Text style={[styles.textLabel, styles.textMicroBold]}>{translate(nextStep.title === CONST.NEXT_STEP.FINISHED ? 'iou.finished' : 'iou.nextStep')}</Text> </View> <View style={[styles.dFlex, styles.flexRow, styles.flexShrink1]}> diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 8cd1e8b57bd2..9cbcf1dd3697 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -192,7 +192,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, title={translate('iou.hold')} description={translate('iou.requestOnHold')} shouldShowBorderBottom - badgeColorStyle={styles.badgeDanger} + danger /> )} </View> diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index 9bbee03b0926..2ca8195c6d3f 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import Text from './Text'; @@ -14,15 +13,15 @@ type MoneyRequestHeaderStatusBarProps = { /** Whether we show the border bottom */ shouldShowBorderBottom: boolean; - /** Badge background color Style */ - badgeColorStyle?: StyleProp<ViewStyle>; + /** Red Badge background */ + danger?: boolean; }; -function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom, badgeColorStyle}: MoneyRequestHeaderStatusBarProps) { +function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom, danger}: MoneyRequestHeaderStatusBarProps) { const styles = useThemeStyles(); const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; - const badgeBackgroundColorStyle = badgeColorStyle ?? styles.moneyRequestHeaderStatusBarBadgeBackground; - const badgeTextColorStyle = badgeColorStyle ? styles.textMicroBoldDangerColor : styles.textMicroBoldColor; + const badgeBackgroundColorStyle = danger ? styles.moneyRequestHeaderStatusBarBadgeDangerBackground : styles.moneyRequestHeaderStatusBarBadgeBackground; + const badgeTextColorStyle = danger ? styles.textMicroBoldDangerColor : styles.textMicroBoldColor; return ( <View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.flexGrow1, styles.overflowHidden, styles.ph5, styles.pb3, borderBottomStyle]}> diff --git a/src/styles/index.ts b/src/styles/index.ts index 85af51afb6e9..41b0c786c03a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4181,6 +4181,10 @@ const styles = (theme: ThemeColors) => backgroundColor: theme.border, }, + moneyRequestHeaderStatusBarBadgeDangerBackground: { + backgroundColor: theme.danger, + }, + staticHeaderImage: { minHeight: 240, }, From 71c86a2e6c476e4b0eab04e57fef7aaa5569b9f4 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Thu, 28 Mar 2024 16:32:47 +0100 Subject: [PATCH 028/580] fix navigation on lost/fraud/actvate card flows --- src/ROUTES.ts | 12 ++++---- src/libs/Navigation/types.ts | 23 ++++++++++++--- .../Wallet/ActivatePhysicalCardPage.tsx | 28 +++++++++---------- .../settings/Wallet/ExpensifyCardPage.tsx | 6 ++-- .../settings/Wallet/ReportCardLostPage.tsx | 14 ++++------ .../Wallet/ReportVirtualCardFraudPage.tsx | 16 +++++------ 6 files changed, 54 insertions(+), 45 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1c225df3a0ec..c7e28bb1f070 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -88,8 +88,8 @@ const ROUTES = { getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}` as const, }, SETTINGS_REPORT_FRAUD: { - route: 'settings/wallet/card/:domain/report-virtual-fraud', - getRoute: (domain: string) => `settings/wallet/card/${domain}/report-virtual-fraud` as const, + route: 'settings/wallet/card/:domain/:cardId/report-virtual-fraud', + getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}/report-virtual-fraud` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: { route: 'settings/wallet/card/:domain/get-physical/name', @@ -117,12 +117,12 @@ const ROUTES = { SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { - route: 'settings/wallet/card/:domain/report-card-lost-or-damaged', - getRoute: (domain: string) => `settings/wallet/card/${domain}/report-card-lost-or-damaged` as const, + route: 'settings/wallet/card/:domain/:cardId/report-card-lost-or-damaged', + getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}/report-card-lost-or-damaged` as const, }, SETTINGS_WALLET_CARD_ACTIVATE: { - route: 'settings/wallet/card/:domain/activate', - getRoute: (domain: string) => `settings/wallet/card/${domain}/activate` as const, + route: 'settings/wallet/card/:domain/:cardId/activate', + getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}/activate` as const, }, SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 2de7c6ab11f7..9250f4e7131c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -117,10 +117,20 @@ type SettingsNavigatorParamList = { /** cardId passed via route /settings/wallet/card/:domain/:card */ cardId: string; }; - [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: undefined; - [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: undefined; + [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: { + /** domain passed via route /settings/wallet/card/:domain/:card */ + domain: string; + /** cardId passed via route /settings/wallet/card/:domain/:card */ + cardId: string; + }; + [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: { + /** domain passed via route /settings/wallet/card/:domain/:card */ + domain: string; + /** cardId passed via route /settings/wallet/card/:domain/:card */ + cardId: string; + }; [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: { - /** domain passed via route /settings/wallet/card/:domain */ + /** domain passed via route /settings/wallet/card/:domain/:card */ domain: string; }; [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE]: { @@ -255,7 +265,12 @@ type SettingsNavigatorParamList = { backTo: Routes; }; [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: BackToParams; - [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: undefined; + [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: { + /** domain passed via route /settings/wallet/card/:domain/:card */ + domain: string; + /** cardId passed via route /settings/wallet/card/:domain/:card */ + cardId: string; + }; [SCREENS.KEYBOARD_SHORTCUTS]: undefined; [SCREENS.SETTINGS.EXIT_SURVEY.REASON]: undefined; [SCREENS.SETTINGS.EXIT_SURVEY.RESPONSE]: { diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index d5b1230fd216..2dada28a6df0 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -19,7 +19,7 @@ import * as CardUtils from '@libs/CardUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {PublicScreensParamList} from '@libs/Navigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as CardSettings from '@userActions/Card'; import CONST from '@src/CONST'; @@ -34,7 +34,7 @@ type ActivatePhysicalCardPageOnyxProps = { cardList: OnyxEntry<Record<string, Card>>; }; -type ActivatePhysicalCardPageProps = ActivatePhysicalCardPageOnyxProps & StackScreenProps<PublicScreensParamList, typeof SCREENS.TRANSITION_BETWEEN_APPS>; +type ActivatePhysicalCardPageProps = ActivatePhysicalCardPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.WALLET.CARD_ACTIVATE>; const LAST_FOUR_DIGITS_LENGTH = 4; const MAGIC_INPUT_MIN_HEIGHT = 86; @@ -42,7 +42,7 @@ const MAGIC_INPUT_MIN_HEIGHT = 86; function ActivatePhysicalCardPage({ cardList, route: { - params: {domain = ''}, + params: {domain = '', cardId = ''}, }, }: ActivatePhysicalCardPageProps) { const theme = useTheme(); @@ -56,8 +56,7 @@ function ActivatePhysicalCardPage({ const [lastPressedDigit, setLastPressedDigit] = useState(''); const domainCards = CardUtils.getDomainCards(cardList)[domain] ?? []; - const physicalCard = domainCards.find((card) => !card.isVirtual); - const cardID = physicalCard?.cardID ?? 0; + const physicalCard = domainCards.find((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED); const cardError = ErrorUtils.getLatestErrorMessage(physicalCard ?? {}); const activateCardCodeInputRef = useRef<MagicCodeInputHandle>(null); @@ -66,19 +65,18 @@ function ActivatePhysicalCardPage({ * If state of the card is CONST.EXPENSIFY_CARD.STATE.OPEN, navigate to card details screen. */ useEffect(() => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (physicalCard?.isLoading || cardList?.[cardID]?.state !== CONST.EXPENSIFY_CARD.STATE.OPEN) { + if (physicalCard?.state !== CONST.EXPENSIFY_CARD.STATE.OPEN || physicalCard?.isLoading) { return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain)); - }, [cardID, cardList, domain, physicalCard?.isLoading]); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId)); + }, [cardId, cardList, domain, physicalCard?.isLoading, physicalCard?.state]); useEffect( () => () => { - CardSettings.clearCardListErrors(cardID); + CardSettings.clearCardListErrors(physicalCard?.cardID ?? 0); }, - [cardID], + [physicalCard?.cardID], ); /** @@ -96,7 +94,7 @@ function ActivatePhysicalCardPage({ setFormError(''); if (cardError) { - CardSettings.clearCardListErrors(cardID); + CardSettings.clearCardListErrors(physicalCard?.cardID ?? 0); } setLastFourDigits(text); @@ -110,8 +108,8 @@ function ActivatePhysicalCardPage({ return; } - CardSettings.activatePhysicalExpensifyCard(lastFourDigits, cardID); - }, [lastFourDigits, cardID]); + CardSettings.activatePhysicalExpensifyCard(lastFourDigits, physicalCard?.cardID ?? 0); + }, [lastFourDigits, physicalCard?.cardID]); if (isEmptyObject(physicalCard)) { return <NotFoundPage />; @@ -120,7 +118,7 @@ function ActivatePhysicalCardPage({ return ( <IllustratedHeaderPageLayout title={translate('activateCardPage.activateCard')} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId))} backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.PREFERENCES.ROOT].backgroundColor} illustration={LottieAnimations.Magician} scrollViewContainerStyles={[styles.mnh100]} diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 80f134fc575a..9c27ddde77b2 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -249,7 +249,7 @@ function ExpensifyCardPage({ titleStyle={styles.walletCardMenuItem} icon={Expensicons.Flag} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain, cardId))} /> </> ))} @@ -267,7 +267,7 @@ function ExpensifyCardPage({ title={translate('reportCardLostOrDamaged.report')} icon={Expensicons.Flag} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain, cardId))} /> </> ), @@ -280,7 +280,7 @@ function ExpensifyCardPage({ success large style={[styles.w100, styles.p5]} - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(domain))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(domain, cardId))} text={translate('activateCardPage.activatePhysicalCard')} /> )} diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index f53ebfb65e3f..991c63d1d760 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -13,9 +13,8 @@ import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CardUtils from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {PublicScreensParamList} from '@libs/Navigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import type {ReplacementReason} from '@userActions/Card'; @@ -62,7 +61,7 @@ type ReportCardLostPageOnyxProps = { cardList: OnyxEntry<Record<string, Card>>; }; -type ReportCardLostPageProps = ReportCardLostPageOnyxProps & StackScreenProps<PublicScreensParamList, typeof SCREENS.TRANSITION_BETWEEN_APPS>; +type ReportCardLostPageProps = ReportCardLostPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED>; function ReportCardLostPage({ privatePersonalDetails = { @@ -77,15 +76,14 @@ function ReportCardLostPage({ }, cardList = {}, route: { - params: {domain = ''}, + params: {domain = '', cardId = ''}, }, formData, }: ReportCardLostPageProps) { const styles = useThemeStyles(); usePrivatePersonalDetails(); - const domainCards = CardUtils.getDomainCards(cardList ?? {})[domain]; - const physicalCard = CardUtils.findPhysicalCard(domainCards); + const physicalCard = cardList?.[cardId]; const {translate} = useLocalize(); @@ -103,8 +101,8 @@ function ReportCardLostPage({ return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain)); - }, [domain, formData?.isLoading, prevIsLoading, physicalCard?.errors]); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId)); + }, [domain, formData?.isLoading, prevIsLoading, physicalCard?.errors, cardId]); useEffect(() => { if (formData?.isLoading && isEmptyObject(physicalCard?.errors)) { diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx index 40a3eacb4ed9..1706c9bef510 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx @@ -10,10 +10,9 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CardUtils from '@libs/CardUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import type {PublicScreensParamList} from '@libs/Navigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Card from '@userActions/Card'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -31,11 +30,11 @@ type ReportVirtualCardFraudPageOnyxProps = { cardList: OnyxEntry<Record<string, OnyxCard>>; }; -type ReportVirtualCardFraudPageProps = ReportVirtualCardFraudPageOnyxProps & StackScreenProps<PublicScreensParamList, typeof SCREENS.TRANSITION_BETWEEN_APPS>; +type ReportVirtualCardFraudPageProps = ReportVirtualCardFraudPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD>; function ReportVirtualCardFraudPage({ route: { - params: {domain = ''}, + params: {domain = '', cardId = ''}, }, cardList, formData, @@ -43,8 +42,7 @@ function ReportVirtualCardFraudPage({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const domainCards = CardUtils.getDomainCards(cardList)[domain]; - const virtualCard = domainCards?.find((card) => card.isVirtual); + const virtualCard = cardList?.[cardId]; const virtualCardError = ErrorUtils.getLatestErrorMessage(virtualCard?.errors ?? {}); const prevIsLoading = usePrevious(formData?.isLoading); @@ -57,8 +55,8 @@ function ReportVirtualCardFraudPage({ return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain)); - }, [domain, formData?.isLoading, prevIsLoading, virtualCard?.errors]); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId)); + }, [cardId, domain, formData?.isLoading, prevIsLoading, virtualCard?.errors]); if (isEmptyObject(virtualCard)) { return <NotFoundPage />; @@ -68,7 +66,7 @@ function ReportVirtualCardFraudPage({ <ScreenWrapper testID={ReportVirtualCardFraudPage.displayName}> <HeaderWithBackButton title={translate('reportFraudPage.title')} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId))} /> <View style={[styles.flex1, styles.justifyContentBetween]}> <Text style={[styles.webViewStyles.baseFontStyle, styles.mh5]}>{translate('reportFraudPage.description')}</Text> From 13674cd7ec9215792368a7a48f0682b63c6a9fd2 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Thu, 28 Mar 2024 16:33:08 +0100 Subject: [PATCH 029/580] fix navigation on get physical card flow --- .../Wallet/Card/BaseGetPhysicalCard.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index bcd1c1096b0e..1d80378e7758 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -109,24 +109,25 @@ function BaseGetPhysicalCard({ const styles = useThemeStyles(); const isRouteSet = useRef(false); + const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; + const physicalCard = domainCards.find((card) => !card?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); + const cardID = physicalCard?.cardID ?? 0; + useEffect(() => { if (isRouteSet.current || !privatePersonalDetails || !cardList) { return; } - const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; - const physicalCard = domainCards.find((card) => !card?.isVirtual); - // When there are no cards for the specified domain, user is redirected to the wallet page - if (domainCards.length === 0) { + if (domainCards.length === 0 || !physicalCard) { Navigation.goBack(ROUTES.SETTINGS_WALLET); return; } // When there's no physical card or it exists but it doesn't have the required state for this flow, // redirect user to the espensify card page - if (!physicalCard || physicalCard.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED) { - Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain)); + if (physicalCard.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED) { + Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, physicalCard.cardID.toString())); return; } @@ -141,25 +142,21 @@ function BaseGetPhysicalCard({ // Redirect user to previous steps of the flow if he hasn't finished them yet GetPhysicalCardUtils.setCurrentRoute(currentRoute, domain, GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues)); isRouteSet.current = true; - }, [cardList, currentRoute, domain, draftValues, loginList, privatePersonalDetails]); + }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, physicalCard, privatePersonalDetails]); const onSubmit = useCallback(() => { const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues); // If the current step of the get physical card flow is the confirmation page if (isConfirmation) { - const domainCards = CardUtils.getDomainCards(cardList)[domain]; - const physicalCard = domainCards.find((card) => !card?.isVirtual); - const cardID = physicalCard?.cardID ?? 0; - Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken ?? '', updatedPrivatePersonalDetails); // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain)); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID.toString())); return; } GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); - }, [cardList, domain, draftValues, isConfirmation, session?.authToken]); + }, [cardID, domain, draftValues, isConfirmation, session?.authToken]); return ( <ScreenWrapper shouldEnablePickerAvoiding={false} @@ -168,7 +165,7 @@ function BaseGetPhysicalCard({ > <HeaderWithBackButton title={title} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID.toString()))} /> <Text style={[styles.textHeadline, styles.mh5, styles.mb5]}>{headline}</Text> {renderContent({onSubmit, submitButtonText, children, onValidate})} From b785e1fd5045acaeed324b8115abeedda2f81144 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Fri, 29 Mar 2024 10:19:13 +0530 Subject: [PATCH 030/580] show notes violation to everyone. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- .../ReportActionItem/MoneyRequestView.tsx | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d3a3584e2ae4..ac7efd88fcfd 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -41,9 +41,6 @@ import type {TransactionPendingFieldsKey} from '@src/types/onyx/Transaction'; import ReportActionItemImage from './ReportActionItemImage'; type MoneyRequestViewTransactionOnyxProps = { - /** Session info for the currently logged in user. */ - session: OnyxEntry<OnyxTypes.Session>; - /** The transaction associated with the transactionThread */ transaction: OnyxEntry<OnyxTypes.Transaction>; @@ -88,7 +85,6 @@ function MoneyRequestView({ policyTagList, policy, transactionViolations, - session, }: MoneyRequestViewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -135,10 +131,6 @@ function MoneyRequestView({ const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); - const isActionOwner = typeof parentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && parentReportAction.actorAccountID === session?.accountID; - const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; - // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); @@ -160,7 +152,7 @@ function MoneyRequestView({ [canUseViolations, getViolationsForField], ); const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); - const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report) && (isActionOwner || isPolicyAdmin || isApprover); + const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report); let amountDescription = `${translate('iou.amount')}`; @@ -501,8 +493,5 @@ export default withOnyx<MoneyRequestViewPropsWithoutTransaction, MoneyRequestVie return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`; }, }, - session: { - key: ONYXKEYS.SESSION, - }, })(MoneyRequestView), ); From f4e118a4d1fd5f57240d6aa992fff78c094d04e3 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Fri, 29 Mar 2024 10:52:43 +0530 Subject: [PATCH 031/580] fix: violation messages styles. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ViolationMessages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ViolationMessages.tsx b/src/components/ViolationMessages.tsx index 0cbcc1051537..e30ad251e0a2 100644 --- a/src/components/ViolationMessages.tsx +++ b/src/components/ViolationMessages.tsx @@ -15,7 +15,7 @@ export default function ViolationMessages({violations, isLast, containerStyle, t const violationMessages = useMemo(() => violations.map((violation) => [violation.name, ViolationsUtils.getViolationTranslation(violation, translate)]), [translate, violations]); return ( - <View style={[styles.mtn2, isLast ? styles.mb2 : styles.mb1, containerStyle]}> + <View style={[styles.mtn1, isLast ? styles.mb2 : styles.mb1, containerStyle, styles.gap1]}> {violationMessages.map(([name, message]) => ( <Text key={`violationMessages.${name}`} From 80b7eb55cea2f23027d80c27773450ec6767421f Mon Sep 17 00:00:00 2001 From: Krishna <belivethatkg@gmail.com> Date: Sun, 31 Mar 2024 11:37:51 +0530 Subject: [PATCH 032/580] Update MoneyRequestView.tsx --- src/components/ReportActionItem/MoneyRequestView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 453e75df9b5a..eb918ed8facf 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -248,7 +248,6 @@ function MoneyRequestView({ <View style={[StyleUtils.getReportWelcomeContainerStyle(isSmallScreenWidth, true, shouldShowAnimatedBackground)]}> {shouldShowAnimatedBackground && <AnimatedEmptyStateBackground />} <View style={shouldShowAnimatedBackground && [StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth, true)]}> - <View style={[StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth)]}> {hasReceipt && ( <ReceiptAuditHeader notes={noteTypeViolations} From bfec360250118e5c871c44408f149a38955355e5 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Sun, 31 Mar 2024 11:42:16 +0530 Subject: [PATCH 033/580] remove ReceiptAuditHeader condition. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index eb918ed8facf..1adf9582d538 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -248,13 +248,10 @@ function MoneyRequestView({ <View style={[StyleUtils.getReportWelcomeContainerStyle(isSmallScreenWidth, true, shouldShowAnimatedBackground)]}> {shouldShowAnimatedBackground && <AnimatedEmptyStateBackground />} <View style={shouldShowAnimatedBackground && [StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth, true)]}> - {hasReceipt && ( - <ReceiptAuditHeader - notes={noteTypeViolations} - showAuditMessage={shouldShowNotesViolations} - /> - )} - + <ReceiptAuditHeader + notes={noteTypeViolations} + showAuditMessage={shouldShowNotesViolations} + /> {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {(showMapAsImage || hasReceipt) && ( <OfflineWithFeedback From e5d8cc5c6dfc5f44713e48362854cf1fe0459a3b Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Tue, 2 Apr 2024 12:02:31 +0200 Subject: [PATCH 034/580] fix PR comments --- src/libs/Navigation/types.ts | 24 ++-- .../settings/Wallet/ExpensifyCardPage.tsx | 131 +++++++++--------- 2 files changed, 77 insertions(+), 78 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9250f4e7131c..d58f0a756ef9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -112,41 +112,41 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.WALLET.ROOT]: undefined; [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: undefined; [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: { - /** domain passed via route /settings/wallet/card/:domain/:card */ + /** domain of selected card */ domain: string; - /** cardId passed via route /settings/wallet/card/:domain/:card */ + /** cardId of selected card */ cardId: string; }; [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: { - /** domain passed via route /settings/wallet/card/:domain/:card */ + /** domain of selected card */ domain: string; - /** cardId passed via route /settings/wallet/card/:domain/:card */ + /** cardId of selected card */ cardId: string; }; [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: { - /** domain passed via route /settings/wallet/card/:domain/:card */ + /** domain of selected card */ domain: string; - /** cardId passed via route /settings/wallet/card/:domain/:card */ + /** cardId of selected card */ cardId: string; }; [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: { - /** domain passed via route /settings/wallet/card/:domain/:card */ + /** domain of selected card */ domain: string; }; [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.PHONE]: { - /** domain passed via route /settings/wallet/card/:domain */ + /** domain of selected card */ domain: string; }; [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.ADDRESS]: { /** Currently selected country */ country: string; - /** domain passed via route /settings/wallet/card/:domain */ + /** domain of selected card */ domain: string; }; [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.CONFIRM]: { /** Currently selected country */ country: string; - /** domain passed via route /settings/wallet/card/:domain */ + /** domain of selected card */ domain: string; }; [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: { @@ -266,9 +266,9 @@ type SettingsNavigatorParamList = { }; [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: BackToParams; [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: { - /** domain passed via route /settings/wallet/card/:domain/:card */ + /** domain of selected card */ domain: string; - /** cardId passed via route /settings/wallet/card/:domain/:card */ + /** cardId of selected card */ cardId: string; }; [SCREENS.KEYBOARD_SHORTCUTS]: undefined; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 9c27ddde77b2..88e1f1953c1e 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -26,6 +26,7 @@ import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Card from '@userActions/Card'; import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -51,6 +52,25 @@ type ExpensifyCardPageOnyxProps = { type ExpensifyCardPageProps = ExpensifyCardPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.WALLET.DOMAIN_CARD>; +type PossibleTitles = 'cardPage.smartLimit.title' | 'cardPage.monthlyLimit.title' | 'cardPage.fixedLimit.title'; + +type LimitTypeTranslationKeys = { + limitNameKey: TranslationPaths; + limitTitleKey: PossibleTitles; +}; + +function getLimitTypeTranslationKeys(limitType: ValueOf<typeof CONST.EXPENSIFY_CARD.LIMIT_TYPES>): LimitTypeTranslationKeys { + // eslint-disable-next-line default-case + switch (limitType) { + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART: + return {limitNameKey: 'cardPage.smartLimit.name', limitTitleKey: 'cardPage.smartLimit.title'}; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY: + return {limitNameKey: 'cardPage.monthlyLimit.name', limitTitleKey: 'cardPage.monthlyLimit.title'}; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED: + return {limitNameKey: 'cardPage.fixedLimit.name', limitTitleKey: 'cardPage.fixedLimit.title'}; + } +} + function ExpensifyCardPage({ cardList, draftValues, @@ -63,13 +83,15 @@ function ExpensifyCardPage({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const isCardDomain = !cardList?.[cardId].isAdminIssuedVirtualCard; + const shouldDisplayCardDomain = !cardList?.[cardId].isAdminIssuedVirtualCard; const [isNotFound, setIsNotFound] = useState(false); - const cardsToShow = useMemo( - () => (isCardDomain ? CardUtils.getDomainCards(cardList)[domain].filter((card) => !card.isAdminIssuedVirtualCard) : [cardList?.[cardId]]), - [isCardDomain, cardList, cardId, domain], - ); + const cardsToShow = useMemo(() => { + if (shouldDisplayCardDomain) { + return CardUtils.getDomainCards(cardList)[domain].filter((card) => !card.isAdminIssuedVirtualCard); + } + return [cardList?.[cardId]]; + }, [shouldDisplayCardDomain, cardList, cardId, domain]); useEffect(() => { setIsNotFound(!cardsToShow); }, [cardList, cardsToShow]); @@ -81,61 +103,36 @@ function ExpensifyCardPage({ const [cardsDetailsErrors, setCardsDetailsErrors] = useState<Record<number, string>>({}); const handleRevealDetails = (revealedCardId: number) => { - setIsCardDetailsLoading((prevState: Record<number, boolean>) => { - const newLoadingStates = {...prevState}; - newLoadingStates[revealedCardId] = true; - return newLoadingStates; - }); + setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({ + ...prevState, + [revealedCardId]: true, + })); // We can't store the response in Onyx for security reasons. // That is why this action is handled manually and the response is stored in a local state // Hence eslint disable here. // eslint-disable-next-line rulesdir/no-thenable-actions-in-views Card.revealVirtualCardDetails(revealedCardId) .then((value) => { - setCardsDetails((prevState: Record<number, TCardDetails | null>) => { - const newCardsDetails = {...prevState}; - newCardsDetails[revealedCardId] = value as TCardDetails; - return newCardsDetails; - }); - setCardsDetailsErrors((prevState) => { - const newCardsDetailsErrors = {...prevState}; - newCardsDetailsErrors[revealedCardId] = ''; - return newCardsDetailsErrors; - }); + setCardsDetails((prevState: Record<number, TCardDetails | null>) => ({...prevState, [revealedCardId]: value as TCardDetails})); + setCardsDetailsErrors((prevState) => ({ + ...prevState, + [revealedCardId]: '', + })); }) .catch((error) => { - setCardsDetailsErrors((prevState) => { - const newCardsDetailsErrors = {...prevState}; - newCardsDetailsErrors[revealedCardId] = error; - return newCardsDetailsErrors; - }); + setCardsDetailsErrors((prevState) => ({ + ...prevState, + [revealedCardId]: error, + })); }) - .finally(() => - setIsCardDetailsLoading((prevState: Record<number, boolean>) => { - const newLoadingStates = {...prevState}; - newLoadingStates[revealedCardId] = false; - return newLoadingStates; - }), - ); + .finally(() => setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({...prevState, [revealedCardId]: false}))); }; const hasDetectedDomainFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN); const hasDetectedIndividualFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL); const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(cardsToShow?.[0]?.availableSpend); - const getLimitStrings = (limitType: ValueOf<typeof CONST.EXPENSIFY_CARD.LIMIT_TYPES>) => { - switch (limitType) { - case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART: - return {limitName: translate('cardPage.smartLimit.name'), limitTitle: translate('cardPage.smartLimit.title', formattedAvailableSpendAmount)}; - case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY: - return {limitName: translate('cardPage.monthlyLimit.name'), limitTitle: translate('cardPage.monthlyLimit.title', formattedAvailableSpendAmount)}; - case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED: - return {limitName: translate('cardPage.fixedLimit.name'), limitTitle: translate('cardPage.fixedLimit.title', formattedAvailableSpendAmount)}; - default: - return {limitName: '', limitTitle: ''}; - } - }; - const {limitName, limitTitle} = getLimitStrings(cardsToShow?.[0]?.limitType); + const {limitNameKey, limitTitleKey} = getLimitTypeTranslationKeys(cardsToShow?.[0]?.limitType); const goToGetPhysicalCardFlow = () => { let updatedDraftValues = draftValues; @@ -203,8 +200,8 @@ function ExpensifyCardPage({ titleStyle={styles.newKansasLarge} /> <MenuItemWithTopDescription - description={limitName} - title={limitTitle} + description={translate(limitNameKey)} + title={translate(limitTitleKey, formattedAvailableSpendAmount)} interactive={false} titleStyle={styles.walletCardLimit} numberOfLinesTitle={3} @@ -253,25 +250,27 @@ function ExpensifyCardPage({ /> </> ))} - {physicalCards.map( - (card) => - card.state === CONST.EXPENSIFY_CARD.STATE.OPEN && ( - <> - <MenuItemWithTopDescription - description={translate('cardPage.physicalCardNumber')} - title={CardUtils.maskCard(card?.lastFourPAN)} - interactive={false} - titleStyle={styles.walletCardNumber} - /> - <MenuItem - title={translate('reportCardLostOrDamaged.report')} - icon={Expensicons.Flag} - shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain, cardId))} - /> - </> - ), - )} + {physicalCards.map((card) => { + if (card.state !== CONST.EXPENSIFY_CARD.STATE.OPEN) { + return null; + } + return ( + <> + <MenuItemWithTopDescription + description={translate('cardPage.physicalCardNumber')} + title={CardUtils.maskCard(card?.lastFourPAN)} + interactive={false} + titleStyle={styles.walletCardNumber} + /> + <MenuItem + title={translate('reportCardLostOrDamaged.report')} + icon={Expensicons.Flag} + shouldShowRightIcon + onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain, cardId))} + /> + </> + ); + })} </> )} </ScrollView> From 7c16aa425e2ee5d83c91524b2bfe54661afd765a Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 3 Apr 2024 12:24:08 +0200 Subject: [PATCH 035/580] refactor: personal info wip --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 4 + .../PersonalInfo/PersonalInfo.tsx | 108 +++++++++++++ .../PersonalInfo/substeps/Address.tsx | 100 ++++++++++++ .../PersonalInfo/substeps/Confirmation.tsx | 146 ++++++++++++++++++ .../PersonalInfo/substeps/DateOfBirth.tsx | 100 ++++++++++++ .../PersonalInfo/substeps/FullName.tsx | 104 +++++++++++++ .../substeps/SocialSecurityNumber.tsx | 91 +++++++++++ 10 files changed, 656 insertions(+) create mode 100644 src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx create mode 100644 src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx create mode 100644 src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx create mode 100644 src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx create mode 100644 src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx create mode 100644 src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4034db2ee0c1..338b8a186515 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -111,6 +111,7 @@ const ROUTES = { SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', SETTINGS_ADD_BANK_ACCOUNT_REFACTOR: 'settings/wallet/add-bank-account-refactor', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', + SETTINGS_ENABLE_PAYMENTS_REFACTOR: 'settings/wallet/enable-payments-refactor', SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: { route: 'settings/wallet/card/:domain/digital-details/update-address', getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c1d6a5669fbc..f07e39051b85 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -79,6 +79,7 @@ const SCREENS = { TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance', CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account', ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', + ENABLE_PAYMENTS_REFACTOR: 'Settings_Wallet_EnablePayments_Refactor', CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index e2f51beb5f8f..28877ef4e098 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -213,6 +213,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: () => require('../../../../pages/settings/Wallet/TransferBalancePage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../../pages/settings/Wallet/ChooseTransferAccountPage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS_REFACTOR]: () => require('../../../../pages/EnablePayments/PersonalInfo/PersonalInfo').default as React.ComponentType, [SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default as React.ComponentType, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_REFACTOR]: () => require('../../../../pages/EnablePayments/AddBankAccount/AddBankAccount').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c777a689b624..ee28ba9b5cc7 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -128,6 +128,10 @@ const config: LinkingOptions<RootStackParamList>['config'] = { path: ROUTES.SETTINGS_ENABLE_PAYMENTS, exact: true, }, + [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS_REFACTOR]: { + path: ROUTES.SETTINGS_ENABLE_PAYMENTS_REFACTOR, + exact: true, + }, [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: { path: ROUTES.SETTINGS_WALLET_TRANSFER_BALANCE, exact: true, diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx new file mode 100644 index 000000000000..43c02e6c104f --- /dev/null +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -0,0 +1,108 @@ +import React, {useEffect, useMemo} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useSubStep from '@hooks/useSubStep'; +import {SubStepProps} from '@hooks/useSubStep/types'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import getInitialSubstepForPersonalInfo from '@pages/ReimbursementAccount/utils/getInitialSubstepForPersonalInfo'; +import * as Wallet from '@userActions/Wallet'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type {UserWallet} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import Address from './substeps/Address'; +import Confirmation from './substeps/Confirmation'; +import DateOfBirth from './substeps/DateOfBirth'; +import FullName from './substeps/FullName'; +import SocialSecurityNumber from './substeps/SocialSecurityNumber'; + +type EnablePaymentsPageOnyxProps = { + /** The user's wallet */ + userWallet: OnyxEntry<UserWallet>; +}; + +type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps; + +const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; +const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, SocialSecurityNumber, Address, Confirmation]; + +function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const styles = useThemeStyles(); + + const {isPendingOnfidoResult, hasFailedOnfido} = userWallet ?? {}; + + const submit = () => {}; + + const values = {}; + + useEffect(() => { + if (isOffline) { + return; + } + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (isPendingOnfidoResult || hasFailedOnfido) { + Navigation.navigate(ROUTES.SETTINGS_WALLET, CONST.NAVIGATION.TYPE.UP); + return; + } + + Wallet.openEnablePaymentsPage(); + }, [isOffline, isPendingOnfidoResult, hasFailedOnfido]); + + const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(values), [values]); + + const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo, goToTheLastStep} = useSubStep({bodyContent, startFrom, onFinished: submit}); + + if (isEmptyObject(userWallet)) { + return <FullScreenLoadingIndicator />; + } + + return ( + <ScreenWrapper + shouldShowOfflineIndicator={userWallet?.currentStep !== CONST.WALLET.STEP.ONFIDO} + includeSafeAreaPaddingBottom={false} + testID={EnablePaymentsPage.displayName} + > + <HeaderWithBackButton + title={translate('personalInfoStep.personalInfo')} + onBackButtonPress={() => Navigation.navigate(ROUTES.SETTINGS)} + /> + <View style={[styles.ph5, styles.mb5, styles.mt3, {height: CONST.BANK_ACCOUNT.STEPS_HEADER_HEIGHT}]}> + <InteractiveStepSubHeader + startStepIndex={1} + stepNames={CONST.WALLET.STEP_NAMES} + /> + </View> + <SubStep + isEditing={isEditing} + onNext={nextScreen} + onMove={moveTo} + /> + </ScreenWrapper> + ); +} + +EnablePaymentsPage.displayName = 'EnablePaymentsPage'; + +export default withOnyx<EnablePaymentsPageProps, EnablePaymentsPageOnyxProps>({ + userWallet: { + key: ONYXKEYS.USER_WALLET, + + // We want to refresh the wallet each time the user attempts to activate the wallet so we won't use the + // stored values here. + initWithStoredValues: false, + }, +})(EnablePaymentsPage); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx new file mode 100644 index 000000000000..712661510b7d --- /dev/null +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx @@ -0,0 +1,100 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import AddressForm from '@pages/ReimbursementAccount/AddressForm'; +import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type {ReimbursementAccount} from '@src/types/onyx'; + +type AddressOnyxProps = { + /** Reimbursement account from ONYX */ + reimbursementAccount: OnyxEntry<ReimbursementAccount>; +}; + +type AddressProps = AddressOnyxProps & SubStepProps; + +const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; + +const INPUT_KEYS = { + street: PERSONAL_INFO_STEP_KEY.STREET, + city: PERSONAL_INFO_STEP_KEY.CITY, + state: PERSONAL_INFO_STEP_KEY.STATE, + zipCode: PERSONAL_INFO_STEP_KEY.ZIP_CODE, +}; + +const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.STREET, PERSONAL_INFO_STEP_KEY.CITY, PERSONAL_INFO_STEP_KEY.STATE, PERSONAL_INFO_STEP_KEY.ZIP_CODE]; + +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) { + errors.requestorAddressStreet = 'bankAccount.error.addressStreet'; + } + + if (values.requestorAddressZipCode && !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) { + errors.requestorAddressZipCode = 'bankAccount.error.zipCode'; + } + + return errors; +}; + +function Address({reimbursementAccount, onNext, isEditing}: AddressProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const defaultValues = { + street: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.STREET] ?? '', + city: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.CITY] ?? '', + state: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.STATE] ?? '', + zipCode: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.ZIP_CODE] ?? '', + }; + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + <FormProvider + formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} + validate={validate} + onSubmit={handleSubmit} + submitButtonStyles={[styles.mb0, styles.pb5]} + style={[styles.mh5, styles.flexGrow1]} + > + <View> + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.enterYourAddress')}</Text> + <Text style={[styles.textSupporting]}>{translate('common.noPO')}</Text> + <AddressForm + inputKeys={INPUT_KEYS} + translate={translate} + streetTranslationKey="common.streetAddress" + defaultValues={defaultValues} + shouldSaveDraft={!isEditing} + /> + <HelpLinks containerStyles={[styles.mt6]} /> + </View> + </FormProvider> + ); +} + +Address.displayName = 'Address'; + +export default withOnyx<AddressProps, AddressOnyxProps>({ + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, +})(Address); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx new file mode 100644 index 000000000000..0e11aff395de --- /dev/null +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx @@ -0,0 +1,146 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import DotIndicatorMessage from '@components/DotIndicatorMessage'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReimbursementAccountForm} from '@src/types/form'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type {ReimbursementAccount} from '@src/types/onyx'; + +type ConfirmationOnyxProps = { + /** Reimbursement account from ONYX */ + reimbursementAccount: OnyxEntry<ReimbursementAccount>; + + /** The draft values of the bank account being setup */ + reimbursementAccountDraft: OnyxEntry<ReimbursementAccountForm>; +}; + +type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; + +const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; +const PERSONAL_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX.PERSONAL_INFO; + +function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext, onMove}: ConfirmationProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + const isLoading = reimbursementAccount?.isLoading ?? false; + const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); + const error = ErrorUtils.getLatestErrorMessage(reimbursementAccount ?? {}); + + return ( + <SafeAreaConsumer> + {({safeAreaPaddingBottomStyle}) => ( + <ScrollView + style={styles.pt0} + contentContainerStyle={[styles.flexGrow1, safeAreaPaddingBottomStyle]} + > + <Text style={[styles.textHeadlineLineHeightXXL, styles.ph5, styles.mb3]}>{translate('personalInfoStep.letsDoubleCheck')}</Text> + <MenuItemWithTopDescription + description={translate('personalInfoStep.legalName')} + title={`${values[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${values[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`} + shouldShowRightIcon + onPress={() => { + onMove(PERSONAL_INFO_STEP_INDEXES.LEGAL_NAME); + }} + /> + <MenuItemWithTopDescription + description={translate('common.dob')} + title={values[PERSONAL_INFO_STEP_KEYS.DOB]} + shouldShowRightIcon + onPress={() => { + onMove(PERSONAL_INFO_STEP_INDEXES.DATE_OF_BIRTH); + }} + /> + <MenuItemWithTopDescription + description={translate('personalInfoStep.last4SSN')} + title={values[PERSONAL_INFO_STEP_KEYS.SSN_LAST_4]} + shouldShowRightIcon + onPress={() => { + onMove(PERSONAL_INFO_STEP_INDEXES.SSN); + }} + /> + <MenuItemWithTopDescription + description={translate('personalInfoStep.address')} + title={`${values[PERSONAL_INFO_STEP_KEYS.STREET]}, ${values[PERSONAL_INFO_STEP_KEYS.CITY]}, ${values[PERSONAL_INFO_STEP_KEYS.STATE]} ${ + values[PERSONAL_INFO_STEP_KEYS.ZIP_CODE] + }`} + shouldShowRightIcon + onPress={() => { + onMove(PERSONAL_INFO_STEP_INDEXES.ADDRESS); + }} + /> + + <Text style={[styles.mt3, styles.ph5, styles.textMicroSupporting]}> + {`${translate('personalInfoStep.byAddingThisBankAccount')} `} + <TextLink + href={CONST.ONFIDO_FACIAL_SCAN_POLICY_URL} + style={[styles.textMicro]} + > + {translate('onfidoStep.facialScan')} + </TextLink> + {', '} + <TextLink + href={CONST.ONFIDO_PRIVACY_POLICY_URL} + style={[styles.textMicro]} + > + {translate('common.privacy')} + </TextLink> + {` ${translate('common.and')} `} + <TextLink + href={CONST.ONFIDO_TERMS_OF_SERVICE_URL} + style={[styles.textMicro]} + > + {translate('common.termsOfService')} + </TextLink> + </Text> + <View style={[styles.ph5, styles.pb5, styles.flexGrow1, styles.justifyContentEnd]}> + {error && error.length > 0 && ( + <DotIndicatorMessage + textStyles={[styles.formError]} + type="error" + messages={{error}} + /> + )} + <Button + isDisabled={isOffline} + success + large + isLoading={isLoading} + style={[styles.w100]} + onPress={onNext} + text={translate('common.confirm')} + /> + </View> + </ScrollView> + )} + </SafeAreaConsumer> + ); +} + +Confirmation.displayName = 'Confirmation'; + +export default withOnyx<ConfirmationProps, ConfirmationOnyxProps>({ + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, + reimbursementAccountDraft: { + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, + }, +})(Confirmation); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx new file mode 100644 index 000000000000..b02a56403641 --- /dev/null +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx @@ -0,0 +1,100 @@ +import {subYears} from 'date-fns'; +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import DatePicker from '@components/DatePicker'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReimbursementAccountForm} from '@src/types/form'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type {ReimbursementAccount} from '@src/types/onyx'; + +type DateOfBirthOnyxProps = { + /** Reimbursement account from ONYX */ + reimbursementAccount: OnyxEntry<ReimbursementAccount>; + + /** The draft values of the bank account being setup */ + reimbursementAccountDraft: OnyxEntry<ReimbursementAccountForm>; +}; + +type DateOfBirthProps = DateOfBirthOnyxProps & SubStepProps; + +const PERSONAL_INFO_DOB_KEY = INPUT_IDS.PERSONAL_INFO_STEP.DOB; +const STEP_FIELDS = [PERSONAL_INFO_DOB_KEY]; + +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.dob) { + if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) { + errors.dob = 'bankAccount.error.dob'; + } else if (!ValidationUtils.meetsMinimumAgeRequirement(values.dob)) { + errors.dob = 'bankAccount.error.age'; + } + } + + return errors; +}; + +function DateOfBirth({reimbursementAccount, reimbursementAccountDraft, onNext, isEditing}: DateOfBirthProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const dobDefaultValue = reimbursementAccount?.achData?.[PERSONAL_INFO_DOB_KEY] ?? reimbursementAccountDraft?.[PERSONAL_INFO_DOB_KEY] ?? ''; + + const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); + const maxDate = subYears(new Date(), CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + <FormProvider + formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} + validate={validate} + onSubmit={handleSubmit} + style={[styles.mh5, styles.flexGrow2, styles.justifyContentBetween]} + submitButtonStyles={[styles.pb5, styles.mb0]} + > + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb5]}>{translate('personalInfoStep.enterYourDateOfBirth')}</Text> + {/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */} + <InputWrapper<unknown> + InputComponent={DatePicker} + inputID={PERSONAL_INFO_DOB_KEY} + label={translate('common.dob')} + placeholder={translate('common.dateFormat')} + defaultValue={dobDefaultValue} + minDate={minDate} + maxDate={maxDate} + shouldSaveDraft={!isEditing} + /> + <HelpLinks containerStyles={[styles.mt5]} /> + </FormProvider> + ); +} + +DateOfBirth.displayName = 'DateOfBirth'; + +export default withOnyx<DateOfBirthProps, DateOfBirthOnyxProps>({ + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, + reimbursementAccountDraft: { + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, + }, +})(DateOfBirth); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx new file mode 100644 index 000000000000..1d225c1c32f2 --- /dev/null +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type {ReimbursementAccount} from '@src/types/onyx'; + +type FullNameOnyxProps = { + /** Reimbursement account from ONYX */ + reimbursementAccount: OnyxEntry<ReimbursementAccount>; +}; + +type FullNameProps = FullNameOnyxProps & SubStepProps; + +const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; +const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME]; + +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + if (values.firstName && !ValidationUtils.isValidLegalName(values.firstName)) { + errors.firstName = 'bankAccount.error.firstName'; + } + + if (values.lastName && !ValidationUtils.isValidLegalName(values.lastName)) { + errors.lastName = 'bankAccount.error.lastName'; + } + return errors; +}; + +function FullName({reimbursementAccount, onNext, isEditing}: FullNameProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const defaultValues = { + firstName: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.FIRST_NAME] ?? '', + lastName: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', + }; + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + <FormProvider + formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} + validate={validate} + onSubmit={handleSubmit} + style={[styles.mh5, styles.flexGrow1]} + submitButtonStyles={[styles.pb5, styles.mb0]} + > + <View> + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb6]}>{translate('personalInfoStep.enterYourLegalFirstAndLast')}</Text> + <View style={[styles.flex2, styles.mb6]}> + <InputWrapper + InputComponent={TextInput} + inputID={PERSONAL_INFO_STEP_KEY.FIRST_NAME} + label={translate('personalInfoStep.legalFirstName')} + aria-label={translate('personalInfoStep.legalFirstName')} + role={CONST.ROLE.PRESENTATION} + defaultValue={defaultValues.firstName} + shouldSaveDraft={!isEditing} + /> + </View> + <View style={[styles.flex2, styles.mb6]}> + <InputWrapper + InputComponent={TextInput} + inputID={PERSONAL_INFO_STEP_KEY.LAST_NAME} + label={translate('personalInfoStep.legalLastName')} + aria-label={translate('personalInfoStep.legalLastName')} + role={CONST.ROLE.PRESENTATION} + defaultValue={defaultValues.lastName} + shouldSaveDraft={!isEditing} + /> + </View> + <HelpLinks /> + </View> + </FormProvider> + ); +} + +FullName.displayName = 'FullName'; + +export default withOnyx<FullNameProps, FullNameOnyxProps>({ + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, +})(FullName); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx new file mode 100644 index 000000000000..e647fd768fb1 --- /dev/null +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type {ReimbursementAccount} from '@src/types/onyx'; + +type SocialSecurityNumberOnyxProps = { + /** Reimbursement account from ONYX */ + reimbursementAccount: OnyxEntry<ReimbursementAccount>; +}; + +type SocialSecurityNumberProps = SocialSecurityNumberOnyxProps & SubStepProps; + +const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; +const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; + +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { + errors.ssnLast4 = 'bankAccount.error.ssnLast4'; + } + + return errors; +}; +function SocialSecurityNumber({reimbursementAccount, onNext, isEditing}: SocialSecurityNumberProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const defaultSsnLast4 = reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? ''; + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + <FormProvider + formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} + validate={validate} + onSubmit={handleSubmit} + style={[styles.mh5, styles.flexGrow1]} + submitButtonStyles={[styles.pb5, styles.mb0]} + > + <View> + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.enterTheLast4')}</Text> + <Text style={[styles.textSupporting]}>{translate('personalInfoStep.dontWorry')}</Text> + <View style={[styles.flex1]}> + <InputWrapper + InputComponent={TextInput} + inputID={PERSONAL_INFO_STEP_KEY.SSN_LAST_4} + label={translate('personalInfoStep.last4SSN')} + aria-label={translate('personalInfoStep.last4SSN')} + role={CONST.ROLE.PRESENTATION} + containerStyles={[styles.mt6]} + inputMode={CONST.INPUT_MODE.NUMERIC} + defaultValue={defaultSsnLast4} + maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.SSN} + shouldSaveDraft={!isEditing} + /> + </View> + <HelpLinks containerStyles={[styles.mt5]} /> + </View> + </FormProvider> + ); +} + +SocialSecurityNumber.displayName = 'SocialSecurityNumber'; + +export default withOnyx<SocialSecurityNumberProps, SocialSecurityNumberOnyxProps>({ + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + reimbursementAccount: { + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + }, +})(SocialSecurityNumber); From ae2324e5ad5dab3b5f206820bf66c89be236d0f0 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Thu, 4 Apr 2024 10:48:22 +0530 Subject: [PATCH 036/580] fix: margins issue. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 7 +++---- src/styles/index.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 3ec5e7f8e6de..7100e1a60842 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -14,13 +14,12 @@ function ReceiptAuditHeader({notes = [], showAuditMessage = false}: {notes?: str const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('common.verified'); return ( - <View style={[styles.ph5, styles.mbn1]}> + <View style={[styles.ph5, styles.mb1]}> <View style={[styles.flexRow, styles.alignItemsCenter]}> <Text style={[styles.textLabelSupporting]}>{translate('common.receipt')}</Text> {showAuditMessage && ( <> - <Text style={[styles.textLabelSupporting, styles.textLarge]}>{' • '}</Text> - <Text style={[styles.textLabelSupporting]}>{`${issuesFoundText}`}</Text> + <Text style={[styles.textLabelSupporting]}>{` • ${issuesFoundText}`}</Text> <Icon width={12} height={12} @@ -37,7 +36,7 @@ function ReceiptAuditHeader({notes = [], showAuditMessage = false}: {notes?: str function ReceiptAuditMessages({notes = []}: {notes?: string[]}) { const styles = useThemeStyles(); - return <View style={[styles.mtn1, styles.mb2, styles.ph5, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View>; + return <View style={[styles.mt1, styles.mb2, styles.ph5, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View>; } export {ReceiptAuditHeader, ReceiptAuditMessages}; diff --git a/src/styles/index.ts b/src/styles/index.ts index a736bc537fa6..d9a5a6e46ac5 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4231,7 +4231,7 @@ const styles = (theme: ThemeColors) => }, moneyRequestViewImage: { ...spacing.mh5, - ...spacing.mv3, + ...spacing.mv1, overflow: 'hidden', borderWidth: 2, borderColor: theme.cardBG, From fa0c505fb4047067a4bcd50fe86a80b96a0a1bb9 Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Thu, 4 Apr 2024 18:47:04 +0700 Subject: [PATCH 037/580] fix login page appear for a while --- src/CONST.ts | 5 +++++ src/libs/NetworkConnection.ts | 11 +++++++++++ src/libs/actions/Network.ts | 7 ++++++- src/libs/actions/Report.ts | 5 ++++- src/types/onyx/Network.ts | 5 +++++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 49019170da66..443eb80d3a00 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -978,6 +978,11 @@ const CONST = { PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, MAX_REQUEST_RETRIES: 10, + NETWORK_STATUS: { + ONLINE: 'online', + OFFLINE: 'offline', + UNKNOWN: 'unknown', + }, }, WEEK_STARTS_ON: 1, // Monday DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index 1cf37623ef2e..2077a168b4b9 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -1,6 +1,8 @@ import NetInfo from '@react-native-community/netinfo'; +import {isBoolean} from 'lodash'; import throttle from 'lodash/throttle'; import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -10,6 +12,7 @@ import Log from './Log'; let isOffline = false; let hasPendingNetworkCheck = false; +type NetworkStatus = ValueOf<typeof CONST.NETWORK.NETWORK_STATUS>; // Holds all of the callbacks that need to be triggered when the network reconnects let callbackID = 0; @@ -107,6 +110,13 @@ function subscribeToNetInfo(): void { return; } setOfflineStatus((state.isInternetReachable ?? false) === false); + let networkStatus; + if (!isBoolean(state.isInternetReachable)) { + networkStatus = CONST.NETWORK.NETWORK_STATUS.UNKNOWN; + } else { + networkStatus = state.isInternetReachable ? CONST.NETWORK.NETWORK_STATUS.ONLINE : CONST.NETWORK.NETWORK_STATUS.OFFLINE; + } + NetworkActions.setNetWorkStatus(networkStatus); }); } @@ -158,3 +168,4 @@ export default { recheckNetworkConnection, subscribeToNetInfo, }; +export type {NetworkStatus}; diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index e71094eded05..afd18f801e8c 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -1,10 +1,15 @@ import Onyx from 'react-native-onyx'; +import type {NetworkStatus} from '@libs/NetworkConnection'; import ONYXKEYS from '@src/ONYXKEYS'; function setIsOffline(isOffline: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {isOffline}); } +function setNetWorkStatus(status: NetworkStatus) { + Onyx.merge(ONYXKEYS.NETWORK, {networkStatus: status}); +} + function setTimeSkew(skew: number) { Onyx.merge(ONYXKEYS.NETWORK, {timeSkew: skew}); } @@ -20,4 +25,4 @@ function setShouldFailAllRequests(shouldFailAllRequests: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {shouldFailAllRequests}); } -export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew}; +export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew, setNetWorkStatus}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f5315f2c8006..293bb5a31d81 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -49,6 +49,7 @@ import * as Environment from '@libs/Environment/Environment'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import type {NetworkStatus} from '@libs/NetworkConnection'; import LocalNotification from '@libs/Notification/LocalNotification'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; @@ -151,10 +152,12 @@ Onyx.connect({ }); let isNetworkOffline = false; +let networkStatus: NetworkStatus; Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (value) => { isNetworkOffline = value?.isOffline ?? false; + networkStatus = value?.networkStatus ?? CONST.NETWORK.NETWORK_STATUS.UNKNOWN; }, }); @@ -2245,7 +2248,7 @@ function openReportFromDeepLink(url: string) { openReport(reportID, '', [], {}, '0', true); // Show the sign-in page if the app is offline - if (isNetworkOffline) { + if (networkStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE) { Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false); } } else { diff --git a/src/types/onyx/Network.ts b/src/types/onyx/Network.ts index 173ca486b53c..cdfa7e02c8f6 100644 --- a/src/types/onyx/Network.ts +++ b/src/types/onyx/Network.ts @@ -1,3 +1,5 @@ +import type {NetworkStatus} from '@libs/NetworkConnection'; + type Network = { /** Is the network currently offline or not */ isOffline: boolean; @@ -10,6 +12,9 @@ type Network = { /** Skew between the client and server clocks */ timeSkew?: number; + + /** The network's status */ + networkStatus?: NetworkStatus; }; export default Network; From 18d1650d1706f045b1c55c2519cb47ffb26657a8 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 4 Apr 2024 18:55:57 +0700 Subject: [PATCH 038/580] fix: Generate Workspace Avatar Color based on PolicyID instead of Workspace Name --- src/components/Avatar.tsx | 8 ++++++-- src/components/HeaderWithBackButton/index.tsx | 1 + src/components/MentionSuggestions.tsx | 1 + src/components/MultipleAvatars.tsx | 4 ++++ src/components/RoomHeaderAvatars.tsx | 2 ++ src/components/SubscriptAvatar.tsx | 2 ++ src/components/WorkspaceSwitcherButton.tsx | 2 +- src/libs/ReportUtils.ts | 4 ++-- src/pages/WorkspaceSwitcherPage.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 1 + src/pages/workspace/WorkspaceInitialPage.tsx | 1 + src/pages/workspace/WorkspaceProfilePage.tsx | 3 ++- src/pages/workspace/WorkspacesListPage.tsx | 1 + src/pages/workspace/WorkspacesListRow.tsx | 5 +++++ src/styles/utils/index.ts | 4 ++-- 15 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 2b2d0a60f657..62d62a4a1760 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -49,6 +49,9 @@ type AvatarProps = { /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; + + /** ID of the policy */ + policyID?: number | string; }; function Avatar({ @@ -62,6 +65,7 @@ function Avatar({ fallbackIconTestID = '', type = CONST.ICON_TYPE_AVATAR, name = '', + policyID, }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -84,7 +88,7 @@ function Avatar({ const imageStyle: StyleProp<ImageStyle> = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; - const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(name).fill : fill; + const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(policyID?.toString() ?? '').fill : fill; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; @@ -110,7 +114,7 @@ function Avatar({ fill={imageError ? theme.offline : iconFillColor} additionalStyles={[ StyleUtils.getAvatarBorderStyle(size, type), - isWorkspace && StyleUtils.getDefaultWorkspaceAvatarColor(name), + isWorkspace && StyleUtils.getDefaultWorkspaceAvatarColor(policyID?.toString() ?? ''), imageError && StyleUtils.getBackgroundColorStyle(theme.fallbackIconColor), iconAdditionalStyles, ]} diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index f3293596aa46..c20f2fdda1d2 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -124,6 +124,7 @@ function HeaderWithBackButton({ containerStyles={[StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(CONST.AVATAR_SIZE.DEFAULT)), styles.mr3]} source={policyAvatar?.source} name={policyAvatar?.name} + policyID={policyAvatar?.id} type={policyAvatar?.type} /> )} diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 23040a242807..1ed0e6441757 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -72,6 +72,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe source={item.icons[0].source} size={isIcon ? CONST.AVATAR_SIZE.MENTION_ICON : CONST.AVATAR_SIZE.SMALLER} name={item.icons[0].name} + policyID={item.icons[0].id} type={item.icons[0].type} fill={isIcon ? theme.success : undefined} fallbackIcon={item.icons[0].fallbackIcon} diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index dedaba500a9c..14acbd62fc1d 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -156,6 +156,7 @@ function MultipleAvatars({ size={size} fill={icons[0].fill} name={icons[0].name} + policyID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} /> @@ -205,6 +206,7 @@ function MultipleAvatars({ source={icon.source ?? fallbackIcon} size={size} name={icon.name} + policyID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} /> @@ -263,6 +265,7 @@ function MultipleAvatars({ imageStyles={[singleAvatarStyle]} name={icons[0].name} type={icons[0].type} + policyID={icons[0].id} fallbackIcon={icons[0].fallbackIcon} /> </View> @@ -282,6 +285,7 @@ function MultipleAvatars({ size={avatarSize} imageStyles={[singleAvatarStyle]} name={icons[1].name} + policyID={icons[1].id} type={icons[1].type} fallbackIcon={icons[1].fallbackIcon} /> diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index c23108adc0ea..227db04b1b55 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -52,6 +52,7 @@ function RoomHeaderAvatars({icons, reportID, isGroupChat}: RoomHeaderAvatarsProp imageStyles={styles.avatarLarge} size={CONST.AVATAR_SIZE.LARGE} name={icons[0].name} + policyID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} /> @@ -87,6 +88,7 @@ function RoomHeaderAvatars({icons, reportID, isGroupChat}: RoomHeaderAvatarsProp size={CONST.AVATAR_SIZE.LARGE} containerStyles={[...iconStyle, StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type)]} name={icon.name} + policyID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} /> diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 3e0f5fb9785a..744082191884 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -82,6 +82,7 @@ function SubscriptAvatar({ source={mainAvatar?.source} size={size} name={mainAvatar?.name} + policyID={mainAvatar?.id} type={mainAvatar?.type} fallbackIcon={mainAvatar?.fallbackIcon} /> @@ -108,6 +109,7 @@ function SubscriptAvatar({ size={isSmall ? CONST.AVATAR_SIZE.SMALL_SUBSCRIPT : CONST.AVATAR_SIZE.SUBSCRIPT} fill={secondaryAvatar.fill} name={secondaryAvatar.name} + policyID={secondaryAvatar.id} type={secondaryAvatar.type} fallbackIcon={secondaryAvatar.fallbackIcon} /> diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index a94f54682c85..6c0d5c2797e3 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -31,7 +31,7 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { const avatar = policy?.avatar ? policy.avatar : getDefaultWorkspaceAvatar(policy?.name); return { source: avatar, - name: policy?.name ?? '', + name: policy?.id ?? '', type: CONST.ICON_TYPE_WORKSPACE, }; }, [policy]); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 98577e73766a..17719331950b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1653,7 +1653,7 @@ function getWorkspaceIcon(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = source: policyExpenseChatAvatarSource ?? '', type: CONST.ICON_TYPE_WORKSPACE, name: workspaceName, - id: -1, + id: report?.policyID, }; return workspaceIcon; } @@ -1800,7 +1800,7 @@ function getIcons( source: policyExpenseChatAvatarSource, type: CONST.ICON_TYPE_WORKSPACE, name: domainName ?? '', - id: -1, + id: report?.policyID, }; return [domainIcon]; } diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 6f077f764474..28b8f063034a 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -134,7 +134,7 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { { source: policy?.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy?.name), fallbackIcon: Expensicons.FallbackWorkspaceAvatar, - name: policy?.name, + name: policy?.id, type: CONST.ICON_TYPE_WORKSPACE, }, ], diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 696cd7a7d850..5c6edd5ef7a4 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -199,6 +199,7 @@ function ReportActionItemSingle({ source={icon.source} type={icon.type} name={icon.name} + policyID={icon.id} fallbackIcon={fallbackIcon} /> </View> diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 512b637f7f46..88f6555e99ac 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -242,6 +242,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r source: avatar, name: policy?.name ?? '', type: CONST.ICON_TYPE_WORKSPACE, + id: policy.id ?? '', }; }, [policy]); diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 662335d0b358..06b1e95b7bee 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -83,10 +83,11 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi fallbackIcon={Expensicons.FallbackWorkspaceAvatar} size={CONST.AVATAR_SIZE.XLARGE} name={policyName} + policyID={policy?.id ?? ''} type={CONST.ICON_TYPE_WORKSPACE} /> ), - [policy?.avatar, policyName, styles.alignSelfCenter, styles.avatarXLarge], + [policy?.avatar, policy?.id, policyName, styles.alignSelfCenter, styles.avatarXLarge], ); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 549d307b2a2f..9e5f24d8882a 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -200,6 +200,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r {({hovered}) => ( <WorkspacesListRow title={item.title} + policyID={item.policyID} menuItems={threeDotsMenuItems} workspaceIcon={item.icon} ownerAccountID={item.ownerAccountID} diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index 54717b032ebe..d3a3ce3f8399 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -59,6 +59,9 @@ type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { /** Determines if pending column should be shown or not */ isJoinRequestPending?: boolean; + + /** ID of the policy */ + policyID?: string; }; type BrickRoadIndicatorIconProps = { @@ -103,6 +106,7 @@ function WorkspacesListRow({ brickRoadIndicator, shouldDisableThreeDotsMenu, isJoinRequestPending, + policyID, }: WorkspacesListRowProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -144,6 +148,7 @@ function WorkspacesListRow({ source={workspaceIcon} fallbackIcon={fallbackWorkspaceIcon} name={title} + policyID={policyID} type={CONST.ICON_TYPE_WORKSPACE} /> <Text diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index a3357b8982a1..8780329af6ba 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -270,8 +270,8 @@ function getAvatarBorderStyle(size: AvatarSizeName, type: string): ViewStyle { /** * Helper method to return workspace avatar color styles */ -function getDefaultWorkspaceAvatarColor(workspaceName: string): ViewStyle { - const colorHash = UserUtils.hashText(workspaceName.trim(), workspaceColorOptions.length); +function getDefaultWorkspaceAvatarColor(text: string): ViewStyle { + const colorHash = UserUtils.hashText(text.trim(), workspaceColorOptions.length); return workspaceColorOptions[colorHash]; } From b793c1c9fcde8278e393b03a22d6f91a4c028327 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 4 Apr 2024 19:32:18 +0700 Subject: [PATCH 039/580] change name props Avatar --- src/components/Avatar.tsx | 10 +++++----- src/components/HeaderWithBackButton/index.tsx | 2 +- src/components/MentionSuggestions.tsx | 2 +- src/components/MultipleAvatars.tsx | 8 ++++---- src/components/RoomHeaderAvatars.tsx | 4 ++-- src/components/SubscriptAvatar.tsx | 4 ++-- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- src/pages/workspace/WorkspaceProfilePage.tsx | 2 +- src/pages/workspace/WorkspacesListRow.tsx | 2 +- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 62d62a4a1760..af492376a191 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -50,8 +50,8 @@ type AvatarProps = { /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; - /** ID of the policy */ - policyID?: number | string; + /** ID of the Icon */ + iconID?: number | string; }; function Avatar({ @@ -65,7 +65,7 @@ function Avatar({ fallbackIconTestID = '', type = CONST.ICON_TYPE_AVATAR, name = '', - policyID, + iconID, }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -88,7 +88,7 @@ function Avatar({ const imageStyle: StyleProp<ImageStyle> = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; - const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(policyID?.toString() ?? '').fill : fill; + const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(iconID?.toString() ?? '').fill : fill; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; @@ -114,7 +114,7 @@ function Avatar({ fill={imageError ? theme.offline : iconFillColor} additionalStyles={[ StyleUtils.getAvatarBorderStyle(size, type), - isWorkspace && StyleUtils.getDefaultWorkspaceAvatarColor(policyID?.toString() ?? ''), + isWorkspace && StyleUtils.getDefaultWorkspaceAvatarColor(iconID?.toString() ?? ''), imageError && StyleUtils.getBackgroundColorStyle(theme.fallbackIconColor), iconAdditionalStyles, ]} diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index c20f2fdda1d2..17b79a19bde3 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -124,7 +124,7 @@ function HeaderWithBackButton({ containerStyles={[StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(CONST.AVATAR_SIZE.DEFAULT)), styles.mr3]} source={policyAvatar?.source} name={policyAvatar?.name} - policyID={policyAvatar?.id} + iconID={policyAvatar?.id} type={policyAvatar?.type} /> )} diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 1ed0e6441757..009003b2367c 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -72,7 +72,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe source={item.icons[0].source} size={isIcon ? CONST.AVATAR_SIZE.MENTION_ICON : CONST.AVATAR_SIZE.SMALLER} name={item.icons[0].name} - policyID={item.icons[0].id} + iconID={item.icons[0].id} type={item.icons[0].type} fill={isIcon ? theme.success : undefined} fallbackIcon={item.icons[0].fallbackIcon} diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 14acbd62fc1d..cbceace6df16 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -156,7 +156,7 @@ function MultipleAvatars({ size={size} fill={icons[0].fill} name={icons[0].name} - policyID={icons[0].id} + iconID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} /> @@ -206,7 +206,7 @@ function MultipleAvatars({ source={icon.source ?? fallbackIcon} size={size} name={icon.name} - policyID={icon.id} + iconID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} /> @@ -265,7 +265,7 @@ function MultipleAvatars({ imageStyles={[singleAvatarStyle]} name={icons[0].name} type={icons[0].type} - policyID={icons[0].id} + iconID={icons[0].id} fallbackIcon={icons[0].fallbackIcon} /> </View> @@ -285,7 +285,7 @@ function MultipleAvatars({ size={avatarSize} imageStyles={[singleAvatarStyle]} name={icons[1].name} - policyID={icons[1].id} + iconID={icons[1].id} type={icons[1].type} fallbackIcon={icons[1].fallbackIcon} /> diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index 227db04b1b55..5738f58af6ff 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -52,7 +52,7 @@ function RoomHeaderAvatars({icons, reportID, isGroupChat}: RoomHeaderAvatarsProp imageStyles={styles.avatarLarge} size={CONST.AVATAR_SIZE.LARGE} name={icons[0].name} - policyID={icons[0].id} + iconID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} /> @@ -88,7 +88,7 @@ function RoomHeaderAvatars({icons, reportID, isGroupChat}: RoomHeaderAvatarsProp size={CONST.AVATAR_SIZE.LARGE} containerStyles={[...iconStyle, StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type)]} name={icon.name} - policyID={icon.id} + iconID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} /> diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 744082191884..d7ddb9ddf9b0 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -82,7 +82,7 @@ function SubscriptAvatar({ source={mainAvatar?.source} size={size} name={mainAvatar?.name} - policyID={mainAvatar?.id} + iconID={mainAvatar?.id} type={mainAvatar?.type} fallbackIcon={mainAvatar?.fallbackIcon} /> @@ -109,7 +109,7 @@ function SubscriptAvatar({ size={isSmall ? CONST.AVATAR_SIZE.SMALL_SUBSCRIPT : CONST.AVATAR_SIZE.SUBSCRIPT} fill={secondaryAvatar.fill} name={secondaryAvatar.name} - policyID={secondaryAvatar.id} + iconID={secondaryAvatar.id} type={secondaryAvatar.type} fallbackIcon={secondaryAvatar.fallbackIcon} /> diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 5c6edd5ef7a4..ecfa4f603aa7 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -199,7 +199,7 @@ function ReportActionItemSingle({ source={icon.source} type={icon.type} name={icon.name} - policyID={icon.id} + iconID={icon.id} fallbackIcon={fallbackIcon} /> </View> diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 06b1e95b7bee..c328bea9bb96 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -83,7 +83,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi fallbackIcon={Expensicons.FallbackWorkspaceAvatar} size={CONST.AVATAR_SIZE.XLARGE} name={policyName} - policyID={policy?.id ?? ''} + iconID={policy?.id ?? ''} type={CONST.ICON_TYPE_WORKSPACE} /> ), diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index d3a3ce3f8399..dfe91b7d1f8a 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -148,7 +148,7 @@ function WorkspacesListRow({ source={workspaceIcon} fallbackIcon={fallbackWorkspaceIcon} name={title} - policyID={policyID} + iconID={policyID} type={CONST.ICON_TYPE_WORKSPACE} /> <Text From c707b50f47acd879a0b30cae9532a649c0d10303 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Fri, 5 Apr 2024 00:56:34 +0800 Subject: [PATCH 040/580] clear sub step when open bank account page --- .../ReimbursementAccount/ReimbursementAccountPage.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index 0f5d04919e29..bd3290a2be89 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -263,7 +263,7 @@ function ReimbursementAccountPage({reimbursementAccount, route, onfidoToken, pol * Retrieve verified business bank account currently being set up. * @param {boolean} ignoreLocalCurrentStep Pass true if you want the last "updated" view (from db), not the last "viewed" view (from onyx). */ - function fetchData(ignoreLocalCurrentStep) { + function fetchData(ignoreLocalCurrentStep, ignoreLocalSubStep) { // Show loader right away, as optimisticData might be set only later in case multiple calls are in the queue BankAccounts.setReimbursementAccountLoading(true); @@ -272,12 +272,17 @@ function ReimbursementAccountPage({reimbursementAccount, route, onfidoToken, pol const stepToOpen = getStepToOpenFromRouteParams(route); const subStep = achData.subStep || ''; const localCurrentStep = achData.currentStep || ''; - BankAccounts.openReimbursementAccountPage(stepToOpen, subStep, ignoreLocalCurrentStep ? '' : localCurrentStep, policyID); + BankAccounts.openReimbursementAccountPage(stepToOpen, ignoreLocalSubStep ? '' : subStep, ignoreLocalCurrentStep ? '' : localCurrentStep, policyID); } useEffect( () => { - fetchData(); + // If the step to open is empty, we want to clear the sub step, so the connect option view is shown to the user + const isStepToOpenEmpty = getStepToOpenFromRouteParams(route) === ''; + if (isStepToOpenEmpty) { + BankAccounts.setBankAccountSubStep(null); + } + fetchData(false, isStepToOpenEmpty); return () => { BankAccounts.clearReimbursementAccount(); }; From a8032031441bc5d6cff613d50da82e55828cd1ef Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Fri, 5 Apr 2024 11:15:04 +0800 Subject: [PATCH 041/580] add param to jsdoc --- src/pages/ReimbursementAccount/ReimbursementAccountPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index bd3290a2be89..3dbd7681b0c4 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -262,6 +262,7 @@ function ReimbursementAccountPage({reimbursementAccount, route, onfidoToken, pol /** * Retrieve verified business bank account currently being set up. * @param {boolean} ignoreLocalCurrentStep Pass true if you want the last "updated" view (from db), not the last "viewed" view (from onyx). + * @param {boolean} ignoreLocalSubStep Pass true if you want the last "updated" view (from db), not the last "viewed" view (from onyx). */ function fetchData(ignoreLocalCurrentStep, ignoreLocalSubStep) { // Show loader right away, as optimisticData might be set only later in case multiple calls are in the queue From 068e62411f6ee2af6ffb0a3ddc04128061f69478 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Fri, 5 Apr 2024 08:18:54 +0200 Subject: [PATCH 042/580] Revert "Merge pull request #39668 from Expensify/revert-38997-@chrispader/prevent-simultaneous-calls-to-GetMissingOnyxMessages" This reverts commit 3864cdbbcbd672d5d434076c5ebf79c63f1bf4d7, reversing changes made to 42ee04cdeb957c2234ef7f0aad5499fd805f5f3f. --- src/libs/actions/OnyxUpdateManager.ts | 162 ++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 9c6f30cc5e9e..45cb2b78ecde 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -3,6 +3,7 @@ import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxUpdatesFromServer, Response} from '@src/types/onyx'; import * as App from './App'; import * as OnyxUpdates from './OnyxUpdates'; @@ -27,6 +28,134 @@ Onyx.connect({ callback: (value) => (lastUpdateIDAppliedToClient = value), }); +let queryPromise: Promise<Response | Response[] | void> | undefined; + +type DeferredUpdatesDictionary = Record<number, OnyxUpdatesFromServer>; +let deferredUpdates: DeferredUpdatesDictionary = {}; + +// This function will reset the query variables, unpause the SequentialQueue and log an info to the user. +function finalizeUpdatesAndResumeQueue() { + console.debug('[OnyxUpdateManager] Done applying all updates'); + queryPromise = undefined; + deferredUpdates = {}; + Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); + SequentialQueue.unpause(); +} + +// This function applies a list of updates to Onyx in order and resolves when all updates have been applied +const applyUpdates = (updates: DeferredUpdatesDictionary) => Promise.all(Object.values(updates).map((update) => OnyxUpdates.apply(update))); + +// In order for the deferred updates to be applied correctly in order, +// we need to check if there are any gaps between deferred updates. +type DetectGapAndSplitResult = {applicableUpdates: DeferredUpdatesDictionary; updatesAfterGaps: DeferredUpdatesDictionary; latestMissingUpdateID: number | undefined}; +function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSplitResult { + const updateValues = Object.values(updates); + const applicableUpdates: DeferredUpdatesDictionary = {}; + + let gapExists = false; + let firstUpdateAfterGaps: number | undefined; + let latestMissingUpdateID: number | undefined; + + for (const [index, update] of updateValues.entries()) { + const isFirst = index === 0; + + // If any update's previousUpdateID doesn't match the lastUpdateID from the previous update, the deferred updates aren't chained and there's a gap. + // For the first update, we need to check that the previousUpdateID of the fetched update is the same as the lastUpdateIDAppliedToClient. + // For any other updates, we need to check if the previousUpdateID of the current update is found in the deferred updates. + // If an update is chained, we can add it to the applicable updates. + const isChained = isFirst ? update.previousUpdateID === lastUpdateIDAppliedToClient : !!updates[Number(update.previousUpdateID)]; + if (isChained) { + // If a gap exists already, we will not add any more updates to the applicable updates. + // Instead, once there are two chained updates again, we can set "firstUpdateAfterGaps" to the first update after the current gap. + if (gapExists) { + // If "firstUpdateAfterGaps" hasn't been set yet and there was a gap, we need to set it to the first update after all gaps. + if (!firstUpdateAfterGaps) { + firstUpdateAfterGaps = Number(update.previousUpdateID); + } + } else { + // If no gap exists yet, we can add the update to the applicable updates + applicableUpdates[Number(update.lastUpdateID)] = update; + } + } else { + // When we find a (new) gap, we need to set "gapExists" to true and reset the "firstUpdateAfterGaps" variable, + // so that we can continue searching for the next update after all gaps + gapExists = true; + firstUpdateAfterGaps = undefined; + + // If there is a gap, it means the previous update is the latest missing update. + latestMissingUpdateID = Number(update.previousUpdateID); + } + } + + // When "firstUpdateAfterGaps" is not set yet, we need to set it to the last update in the list, + // because we will fetch all missing updates up to the previous one and can then always apply the last update in the deferred updates. + if (!firstUpdateAfterGaps) { + firstUpdateAfterGaps = Number(updateValues[updateValues.length - 1].lastUpdateID); + } + + let updatesAfterGaps: DeferredUpdatesDictionary = {}; + if (gapExists && firstUpdateAfterGaps) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + updatesAfterGaps = Object.fromEntries(Object.entries(updates).filter(([lastUpdateID]) => Number(lastUpdateID) >= firstUpdateAfterGaps!)); + } + + return {applicableUpdates, updatesAfterGaps, latestMissingUpdateID}; +} + +// This function will check for gaps in the deferred updates and +// apply the updates in order after the missing updates are fetched and applied +function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { + // We only want to apply deferred updates that are newer than the last update that was applied to the client. + // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. + const pendingDeferredUpdates = Object.fromEntries( + Object.entries(deferredUpdates).filter(([lastUpdateID]) => { + // It should not be possible for lastUpdateIDAppliedToClient to be null, + // after the missing updates have been applied. + // If still so we want to keep the deferred update in the list. + if (!lastUpdateIDAppliedToClient) { + return true; + } + return (Number(lastUpdateID) ?? 0) > lastUpdateIDAppliedToClient; + }), + ); + + // If there are no remaining deferred updates after filtering out outdated ones, + // we can just unpause the queue and return + if (Object.values(pendingDeferredUpdates).length === 0) { + return Promise.resolve(); + } + + const {applicableUpdates, updatesAfterGaps, latestMissingUpdateID} = detectGapsAndSplit(pendingDeferredUpdates); + + // If we detected a gap in the deferred updates, only apply the deferred updates before the gap, + // re-fetch the missing updates and then apply the remaining deferred updates after the gap + if (latestMissingUpdateID) { + return new Promise((resolve, reject) => { + deferredUpdates = {}; + applyUpdates(applicableUpdates).then(() => { + // After we have applied the applicable updates, there might have been new deferred updates added. + // In the next (recursive) call of "validateAndApplyDeferredUpdates", + // the initial "updatesAfterGaps" and all new deferred updates will be applied in order, + // as long as there was no new gap detected. Otherwise repeat the process. + deferredUpdates = {...deferredUpdates, ...updatesAfterGaps}; + + // It should not be possible for lastUpdateIDAppliedToClient to be null, therefore we can ignore this case. + // If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates. + if (!lastUpdateIDAppliedToClient || latestMissingUpdateID <= lastUpdateIDAppliedToClient) { + validateAndApplyDeferredUpdates().then(resolve).catch(reject); + return; + } + + // Then we can fetch the missing updates and apply them + App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, latestMissingUpdateID).then(validateAndApplyDeferredUpdates).then(resolve).catch(reject); + }); + }); + } + + // If there are no gaps in the deferred updates, we can apply all deferred updates in order + return applyUpdates(applicableUpdates); +} + export default () => { console.debug('[OnyxUpdateManager] Listening for updates from the server'); Onyx.connect({ @@ -66,32 +195,45 @@ export default () => { // applied in their correct and specific order. If this queue was not paused, then there would be a lot of // onyx data being applied while we are fetching the missing updates and that would put them all out of order. SequentialQueue.pause(); - let canUnpauseQueuePromise; // The flow below is setting the promise to a reconnect app to address flow (1) explained above. if (!lastUpdateIDAppliedToClient) { + // If there is a ReconnectApp query in progress, we should not start another one. + if (queryPromise) { + return; + } + Log.info('Client has not gotten reliable updates before so reconnecting the app to start the process'); // Since this is a full reconnectApp, we'll not apply the updates we received - those will come in the reconnect app request. - canUnpauseQueuePromise = App.finalReconnectAppAfterActivatingReliableUpdates(); + queryPromise = App.finalReconnectAppAfterActivatingReliableUpdates(); } else { // The flow below is setting the promise to a getMissingOnyxUpdates to address flow (2) explained above. + + // Get the number of deferred updates before adding the new one + const existingDeferredUpdatesCount = Object.keys(deferredUpdates).length; + + // Add the new update to the deferred updates + deferredUpdates[Number(updateParams.lastUpdateID)] = updateParams; + + // If there are deferred updates already, we don't need to fetch the missing updates again. + if (existingDeferredUpdatesCount > 0) { + return; + } + console.debug(`[OnyxUpdateManager] Client is behind the server by ${Number(previousUpdateIDFromServer) - lastUpdateIDAppliedToClient} so fetching incremental updates`); Log.info('Gap detected in update IDs from server so fetching incremental updates', true, { lastUpdateIDFromServer, previousUpdateIDFromServer, lastUpdateIDAppliedToClient, }); - canUnpauseQueuePromise = App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, previousUpdateIDFromServer); + + // Get the missing Onyx updates from the server and afterwards validate and apply the deferred updates. + // This will trigger recursive calls to "validateAndApplyDeferredUpdates" if there are gaps in the deferred updates. + queryPromise = App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, previousUpdateIDFromServer).then(validateAndApplyDeferredUpdates); } - canUnpauseQueuePromise.finally(() => { - OnyxUpdates.apply(updateParams).finally(() => { - console.debug('[OnyxUpdateManager] Done applying all updates'); - Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); - SequentialQueue.unpause(); - }); - }); + queryPromise.finally(finalizeUpdatesAndResumeQueue); }, }); }; From 1a8528c9179329e295190bf85a3ecf7ce44ad740 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Fri, 5 Apr 2024 11:55:42 +0200 Subject: [PATCH 043/580] remove queue clearing and add TS type guard for OnyxUpdate --- src/libs/actions/OnyxUpdateManager.ts | 22 ++++---------------- src/types/onyx/OnyxUpdatesFromServer.ts | 27 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 45cb2b78ecde..5988c40e589c 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -1,9 +1,9 @@ import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer, Response} from '@src/types/onyx'; +import {isValidOnyxUpdateFromServer} from '@src/types/onyx/OnyxUpdatesFromServer'; import * as App from './App'; import * as OnyxUpdates from './OnyxUpdates'; @@ -25,7 +25,7 @@ import * as OnyxUpdates from './OnyxUpdates'; let lastUpdateIDAppliedToClient: number | null = 0; Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (value) => (lastUpdateIDAppliedToClient = value), + callback: (value: number) => (lastUpdateIDAppliedToClient = value), }); let queryPromise: Promise<Response | Response[] | void> | undefined; @@ -160,22 +160,8 @@ export default () => { console.debug('[OnyxUpdateManager] Listening for updates from the server'); Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, - callback: (value) => { - if (!value) { - return; - } - - // Since we used the same key that used to store another object, let's confirm that the current object is - // following the new format before we proceed. If it isn't, then let's clear the object in Onyx. - if ( - !(typeof value === 'object' && !!value) || - !('type' in value) || - (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && - !((value.type === CONST.ONYX_UPDATE_TYPES.PUSHER || value.type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && value.updates)) - ) { - console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); - Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); - SequentialQueue.unpause(); + callback: (value: unknown) => { + if (!isValidOnyxUpdateFromServer(value)) { return; } diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts index 3c6933da19ba..ca88360cb2b3 100644 --- a/src/types/onyx/OnyxUpdatesFromServer.ts +++ b/src/types/onyx/OnyxUpdatesFromServer.ts @@ -21,4 +21,31 @@ type OnyxUpdatesFromServer = { updates?: OnyxUpdateEvent[]; }; +function isValidOnyxUpdateFromServer(value: unknown): value is OnyxUpdatesFromServer { + if (!value || typeof value !== 'object') { + return false; + } + if (!('type' in value) || !value.type) { + return false; + } + if (value.type === CONST.ONYX_UPDATE_TYPES.HTTPS) { + if (!('request' in value) || !value.request) { + return false; + } + + if (!('response' in value) || !value.response) { + return false; + } + } + if (value.type === CONST.ONYX_UPDATE_TYPES.PUSHER) { + if (!('updates' in value) || !value.updates) { + return false; + } + } + + return true; +} + +export {isValidOnyxUpdateFromServer}; + export type {OnyxUpdatesFromServer, OnyxUpdateEvent, OnyxServerUpdate}; From e52db009eaaddc82963f20ec4c63096499056570 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Fri, 5 Apr 2024 12:01:45 +0200 Subject: [PATCH 044/580] fix: type check --- src/libs/actions/OnyxUpdateManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 5988c40e589c..affc205898f9 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -25,7 +25,7 @@ import * as OnyxUpdates from './OnyxUpdates'; let lastUpdateIDAppliedToClient: number | null = 0; Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (value: number) => (lastUpdateIDAppliedToClient = value), + callback: (value) => (lastUpdateIDAppliedToClient = value), }); let queryPromise: Promise<Response | Response[] | void> | undefined; From 2e13b36fe2903ea4e98d50a87c8da7173e4d614b Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Fri, 5 Apr 2024 12:07:41 +0200 Subject: [PATCH 045/580] simplify code --- src/libs/actions/OnyxUpdateManager.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index affc205898f9..146814599764 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -107,17 +107,12 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSpl function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { // We only want to apply deferred updates that are newer than the last update that was applied to the client. // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. - const pendingDeferredUpdates = Object.fromEntries( - Object.entries(deferredUpdates).filter(([lastUpdateID]) => { - // It should not be possible for lastUpdateIDAppliedToClient to be null, - // after the missing updates have been applied. - // If still so we want to keep the deferred update in the list. - if (!lastUpdateIDAppliedToClient) { - return true; - } - return (Number(lastUpdateID) ?? 0) > lastUpdateIDAppliedToClient; - }), - ); + const pendingDeferredUpdates = Object.entries(deferredUpdates).reduce<DeferredUpdatesDictionary>((acc, [lastUpdateID, update]) => { + if (!lastUpdateIDAppliedToClient || (Number(lastUpdateID) ?? 0) > lastUpdateIDAppliedToClient) { + acc[Number(lastUpdateID)] = update; + } + return acc; + }, {}); // If there are no remaining deferred updates after filtering out outdated ones, // we can just unpause the queue and return From 4fbbd9acde49213e31c1c006abe1a718f0473817 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Fri, 5 Apr 2024 12:12:01 +0200 Subject: [PATCH 046/580] simplify code --- src/libs/actions/OnyxUpdateManager.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 146814599764..c5b4abf5888a 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -95,8 +95,13 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSpl let updatesAfterGaps: DeferredUpdatesDictionary = {}; if (gapExists && firstUpdateAfterGaps) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - updatesAfterGaps = Object.fromEntries(Object.entries(updates).filter(([lastUpdateID]) => Number(lastUpdateID) >= firstUpdateAfterGaps!)); + updatesAfterGaps = Object.entries(updates).reduce<DeferredUpdatesDictionary>((acc, [lastUpdateID, update]) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (Number(lastUpdateID) >= firstUpdateAfterGaps!) { + acc[Number(lastUpdateID)] = update; + } + return acc; + }, {}); } return {applicableUpdates, updatesAfterGaps, latestMissingUpdateID}; From 9eaa455e38a19431ce4a852ba68c1e2f6e7fce20 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Fri, 5 Apr 2024 12:17:01 +0200 Subject: [PATCH 047/580] fix: and simplify --- src/libs/actions/OnyxUpdateManager.ts | 28 +++++++++++++------------ src/types/onyx/OnyxUpdatesFromServer.ts | 1 + 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index c5b4abf5888a..275a904736fe 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -95,13 +95,14 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSpl let updatesAfterGaps: DeferredUpdatesDictionary = {}; if (gapExists && firstUpdateAfterGaps) { - updatesAfterGaps = Object.entries(updates).reduce<DeferredUpdatesDictionary>((acc, [lastUpdateID, update]) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (Number(lastUpdateID) >= firstUpdateAfterGaps!) { - acc[Number(lastUpdateID)] = update; - } - return acc; - }, {}); + updatesAfterGaps = Object.entries(updates).reduce<DeferredUpdatesDictionary>( + (acc, [lastUpdateID, update]) => ({ + ...acc, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...(Number(lastUpdateID) >= firstUpdateAfterGaps! ? {[Number(lastUpdateID)]: update} : {}), + }), + {}, + ); } return {applicableUpdates, updatesAfterGaps, latestMissingUpdateID}; @@ -112,12 +113,13 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSpl function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { // We only want to apply deferred updates that are newer than the last update that was applied to the client. // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. - const pendingDeferredUpdates = Object.entries(deferredUpdates).reduce<DeferredUpdatesDictionary>((acc, [lastUpdateID, update]) => { - if (!lastUpdateIDAppliedToClient || (Number(lastUpdateID) ?? 0) > lastUpdateIDAppliedToClient) { - acc[Number(lastUpdateID)] = update; - } - return acc; - }, {}); + const pendingDeferredUpdates = Object.entries(deferredUpdates).reduce<DeferredUpdatesDictionary>( + (acc, [lastUpdateID, update]) => ({ + ...acc, + ...(!lastUpdateIDAppliedToClient || (Number(lastUpdateID) ?? 0) > lastUpdateIDAppliedToClient ? {[Number(lastUpdateID)]: update} : {}), + }), + {}, + ); // If there are no remaining deferred updates after filtering out outdated ones, // we can just unpause the queue and return diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts index ca88360cb2b3..0877ea6755f8 100644 --- a/src/types/onyx/OnyxUpdatesFromServer.ts +++ b/src/types/onyx/OnyxUpdatesFromServer.ts @@ -1,4 +1,5 @@ import type {OnyxUpdate} from 'react-native-onyx'; +import CONST from '@src/CONST'; import type Request from './Request'; import type Response from './Response'; From c1342830cc59b4a7ce34c52e29096428a837a678 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Fri, 5 Apr 2024 12:20:57 +0200 Subject: [PATCH 048/580] add comment --- src/libs/actions/OnyxUpdateManager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 275a904736fe..d1e2aa3514dc 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -116,6 +116,9 @@ function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { const pendingDeferredUpdates = Object.entries(deferredUpdates).reduce<DeferredUpdatesDictionary>( (acc, [lastUpdateID, update]) => ({ ...acc, + // It should not be possible for lastUpdateIDAppliedToClient to be null, + // after the missing updates have been applied. + // If still so we want to keep the deferred update in the list. ...(!lastUpdateIDAppliedToClient || (Number(lastUpdateID) ?? 0) > lastUpdateIDAppliedToClient ? {[Number(lastUpdateID)]: update} : {}), }), {}, From 36c9ad61dcf7fbe6aa6da925bbb165770da9374a Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Fri, 5 Apr 2024 12:21:40 +0200 Subject: [PATCH 049/580] rename variables --- src/libs/actions/OnyxUpdateManager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index d1e2aa3514dc..7179cb855c90 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -96,8 +96,8 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSpl let updatesAfterGaps: DeferredUpdatesDictionary = {}; if (gapExists && firstUpdateAfterGaps) { updatesAfterGaps = Object.entries(updates).reduce<DeferredUpdatesDictionary>( - (acc, [lastUpdateID, update]) => ({ - ...acc, + (accUpdates, [lastUpdateID, update]) => ({ + ...accUpdates, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ...(Number(lastUpdateID) >= firstUpdateAfterGaps! ? {[Number(lastUpdateID)]: update} : {}), }), @@ -114,8 +114,8 @@ function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { // We only want to apply deferred updates that are newer than the last update that was applied to the client. // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. const pendingDeferredUpdates = Object.entries(deferredUpdates).reduce<DeferredUpdatesDictionary>( - (acc, [lastUpdateID, update]) => ({ - ...acc, + (accUpdates, [lastUpdateID, update]) => ({ + ...accUpdates, // It should not be possible for lastUpdateIDAppliedToClient to be null, // after the missing updates have been applied. // If still so we want to keep the deferred update in the list. From 6bf60e9125231b80d97706c4398a27a5c2b10cf0 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Fri, 5 Apr 2024 14:48:00 +0200 Subject: [PATCH 050/580] make card row title depend on cardTitle when card is admin assigned --- .../settings/Wallet/ExpensifyCardPage.tsx | 32 ++++++++++--------- .../settings/Wallet/PaymentMethodList.tsx | 9 ++++-- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 88e1f1953c1e..c8fddb4fbfb3 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -55,12 +55,11 @@ type ExpensifyCardPageProps = ExpensifyCardPageOnyxProps & StackScreenProps<Sett type PossibleTitles = 'cardPage.smartLimit.title' | 'cardPage.monthlyLimit.title' | 'cardPage.fixedLimit.title'; type LimitTypeTranslationKeys = { - limitNameKey: TranslationPaths; - limitTitleKey: PossibleTitles; + limitNameKey: TranslationPaths | undefined; + limitTitleKey: PossibleTitles | undefined; }; -function getLimitTypeTranslationKeys(limitType: ValueOf<typeof CONST.EXPENSIFY_CARD.LIMIT_TYPES>): LimitTypeTranslationKeys { - // eslint-disable-next-line default-case +function getLimitTypeTranslationKeys(limitType: ValueOf<typeof CONST.EXPENSIFY_CARD.LIMIT_TYPES> | undefined): LimitTypeTranslationKeys { switch (limitType) { case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART: return {limitNameKey: 'cardPage.smartLimit.name', limitTitleKey: 'cardPage.smartLimit.title'}; @@ -68,6 +67,8 @@ function getLimitTypeTranslationKeys(limitType: ValueOf<typeof CONST.EXPENSIFY_C return {limitNameKey: 'cardPage.monthlyLimit.name', limitTitleKey: 'cardPage.monthlyLimit.title'}; case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED: return {limitNameKey: 'cardPage.fixedLimit.name', limitTitleKey: 'cardPage.fixedLimit.title'}; + default: + return {limitNameKey: undefined, limitTitleKey: undefined}; } } @@ -83,12 +84,12 @@ function ExpensifyCardPage({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const shouldDisplayCardDomain = !cardList?.[cardId].isAdminIssuedVirtualCard; + const shouldDisplayCardDomain = !cardList?.[cardId].nameValuePairs?.issuedBy; const [isNotFound, setIsNotFound] = useState(false); const cardsToShow = useMemo(() => { if (shouldDisplayCardDomain) { - return CardUtils.getDomainCards(cardList)[domain].filter((card) => !card.isAdminIssuedVirtualCard); + return CardUtils.getDomainCards(cardList)[domain].filter((card) => !card.nameValuePairs?.issuedBy); } return [cardList?.[cardId]]; }, [shouldDisplayCardDomain, cardList, cardId, domain]); @@ -132,7 +133,7 @@ function ExpensifyCardPage({ const hasDetectedIndividualFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL); const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(cardsToShow?.[0]?.availableSpend); - const {limitNameKey, limitTitleKey} = getLimitTypeTranslationKeys(cardsToShow?.[0]?.limitType); + const {limitNameKey, limitTitleKey} = getLimitTypeTranslationKeys(cardsToShow?.[0]?.nameValuePairs?.limitType); const goToGetPhysicalCardFlow = () => { let updatedDraftValues = draftValues; @@ -199,14 +200,15 @@ function ExpensifyCardPage({ interactive={false} titleStyle={styles.newKansasLarge} /> - <MenuItemWithTopDescription - description={translate(limitNameKey)} - title={translate(limitTitleKey, formattedAvailableSpendAmount)} - interactive={false} - titleStyle={styles.walletCardLimit} - numberOfLinesTitle={3} - /> - + {limitNameKey && limitTitleKey && ( + <MenuItemWithTopDescription + description={translate(limitNameKey)} + title={translate(limitTitleKey, formattedAvailableSpendAmount)} + interactive={false} + titleStyle={styles.walletCardLimit} + numberOfLinesTitle={3} + /> + )} {virtualCards.map((card) => ( <> {!!cardsDetails[card.cardID] && cardsDetails[card.cardID]?.pan ? ( diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index ba1294bb212e..02d8712b6960 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -223,8 +223,10 @@ function PaymentMethodList({ return; } + const isAdminIssuedVirtualCard = !!card.nameValuePairs?.issuedBy; + // The card should be grouped to a specific domain and such domain already exists in a assignedCardsGrouped - if (assignedCardsGrouped.some((item) => item.isGroupedCardDomain && item.description === card.domainName) && !card.isAdminIssuedVirtualCard) { + if (assignedCardsGrouped.some((item) => item.isGroupedCardDomain && item.description === card.domainName) && !isAdminIssuedVirtualCard) { const domainGroupIndex = assignedCardsGrouped.findIndex((item) => item.isGroupedCardDomain && item.description === card.domainName); assignedCardsGrouped[domainGroupIndex].errors = {...assignedCardsGrouped[domainGroupIndex].errors, ...card.errors}; if (card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL) { @@ -236,10 +238,11 @@ function PaymentMethodList({ // The card shouldn't be grouped or it's domain group doesn't exist yet assignedCardsGrouped.push({ key: card.cardID.toString(), - title: card.bank, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + title: isAdminIssuedVirtualCard ? card.nameValuePairs?.cardTitle || card.bank : card.bank, description: card.domainName, onPress: () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName ?? '', card.cardID.toString() ?? '')), - isGroupedCardDomain: !card.isAdminIssuedVirtualCard, + isGroupedCardDomain: !isAdminIssuedVirtualCard, shouldShowRightIcon: true, interactive: true, canDismissError: true, From a10788e73d5db56728914ae80fa7453108f79c7d Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Fri, 5 Apr 2024 15:55:28 +0200 Subject: [PATCH 051/580] adjust expensifyCardPage title --- src/pages/settings/Wallet/ExpensifyCardPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index c8fddb4fbfb3..f862ea1e4dd7 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -85,6 +85,7 @@ function ExpensifyCardPage({ const {isOffline} = useNetwork(); const {translate} = useLocalize(); const shouldDisplayCardDomain = !cardList?.[cardId].nameValuePairs?.issuedBy; + const pageTitle = shouldDisplayCardDomain ? translate('cardPage.expensifyCard') : cardList?.[cardId].nameValuePairs?.cardTitle ?? translate('cardPage.expensifyCard'); const [isNotFound, setIsNotFound] = useState(false); const cardsToShow = useMemo(() => { @@ -159,7 +160,7 @@ function ExpensifyCardPage({ {({safeAreaPaddingBottomStyle}) => ( <> <HeaderWithBackButton - title={translate('cardPage.expensifyCard')} + title={pageTitle} onBackButtonPress={() => Navigation.goBack()} /> <ScrollView contentContainerStyle={safeAreaPaddingBottomStyle}> From 923b75314a10e4300c1c20283c852354b5a2a2ba Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Fri, 5 Apr 2024 15:32:25 -0700 Subject: [PATCH 052/580] Rename docs/articles/expensify-classic/settings/Notification-Troubleshooting.md to docs/articles/expensify-classic/settings/account settings/Notification-Troubleshooting.md Moving this article so it's under the Classic > Settings > Account Settings sub-category. --- .../{ => account settings}/Notification-Troubleshooting.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/settings/{ => account settings}/Notification-Troubleshooting.md (100%) diff --git a/docs/articles/expensify-classic/settings/Notification-Troubleshooting.md b/docs/articles/expensify-classic/settings/account settings/Notification-Troubleshooting.md similarity index 100% rename from docs/articles/expensify-classic/settings/Notification-Troubleshooting.md rename to docs/articles/expensify-classic/settings/account settings/Notification-Troubleshooting.md From d75fe23403c74b676d4fdcd2128bf0fa1246dc56 Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Fri, 5 Apr 2024 15:34:06 -0700 Subject: [PATCH 053/580] Update and rename Notification-Troubleshooting.md to Set-Notifications.md For got the rename of the title as well, adding that in. --- .../{Notification-Troubleshooting.md => Set-Notifications.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/articles/expensify-classic/settings/account settings/{Notification-Troubleshooting.md => Set-Notifications.md} (99%) diff --git a/docs/articles/expensify-classic/settings/account settings/Notification-Troubleshooting.md b/docs/articles/expensify-classic/settings/account settings/Set-Notifications.md similarity index 99% rename from docs/articles/expensify-classic/settings/account settings/Notification-Troubleshooting.md rename to docs/articles/expensify-classic/settings/account settings/Set-Notifications.md index 22df0dc7f6ca..0e18d6f22cf5 100644 --- a/docs/articles/expensify-classic/settings/account settings/Notification-Troubleshooting.md +++ b/docs/articles/expensify-classic/settings/account settings/Set-Notifications.md @@ -1,5 +1,5 @@ --- -title: Notification Troubleshooting +title: Set notifications description: This article is about how to troubleshoot notifications from Expensify. --- From f47ffb44e006c04f30f0026fc8725c84ca1158a5 Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Fri, 5 Apr 2024 15:36:54 -0700 Subject: [PATCH 054/580] Rename docs/articles/expensify-classic/settings/Close-or-reopen-account.md to docs/articles/expensify-classic/settings/account settings/Close-or-reopen-account.md Moving this to be under Classic > Settings > Account Settings per https://github.com/Expensify/Expensify/issues/381301#issuecomment-2021493296. --- .../settings/{ => account settings}/Close-or-reopen-account.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/settings/{ => account settings}/Close-or-reopen-account.md (100%) diff --git a/docs/articles/expensify-classic/settings/Close-or-reopen-account.md b/docs/articles/expensify-classic/settings/account settings/Close-or-reopen-account.md similarity index 100% rename from docs/articles/expensify-classic/settings/Close-or-reopen-account.md rename to docs/articles/expensify-classic/settings/account settings/Close-or-reopen-account.md From 9fb92f176ec53aa2c6f5fcb53709e6851d5d35cb Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Mon, 8 Apr 2024 15:05:32 +0700 Subject: [PATCH 055/580] fix useNetwork --- src/hooks/useNetwork.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts index 9d5e1e75d7c8..79c70771d58d 100644 --- a/src/hooks/useNetwork.ts +++ b/src/hooks/useNetwork.ts @@ -12,7 +12,7 @@ export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = { const callback = useRef(onReconnect); callback.current = onReconnect; - const {isOffline} = useContext(NetworkContext) ?? CONST.DEFAULT_NETWORK_DATA; + const {isOffline, networkStatus} = useContext(NetworkContext) ?? {...CONST.DEFAULT_NETWORK_DATA, networkStatus: CONST.NETWORK.NETWORK_STATUS.UNKNOWN}; const prevOfflineStatusRef = useRef(isOffline); useEffect(() => { // If we were offline before and now we are not offline then we just reconnected @@ -29,5 +29,5 @@ export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = { prevOfflineStatusRef.current = isOffline; }, [isOffline]); - return {isOffline: isOffline ?? false}; + return {isOffline: networkStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE}; } From 4225d83c3f49e4a09ac78c5ad5cf012fc1091bf4 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Tue, 9 Apr 2024 17:14:19 +0530 Subject: [PATCH 056/580] show red dot when review is required. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 961dd93296f6..e92814027ae1 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -97,7 +97,10 @@ function MoneyRequestPreviewContent({ const isSettled = ReportUtils.isSettled(iouReport?.reportID); const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; const shouldShowRBR = - hasViolations || hasFieldErrors || (!(isSettled && !isSettlementOrApprovalPartial) && !(ReportUtils.isReportApproved(iouReport) && !isSettlementOrApprovalPartial) && isOnHold); + hasNoteTypeViolations || + hasViolations || + hasFieldErrors || + (!(isSettled && !isSettlementOrApprovalPartial) && !(ReportUtils.isReportApproved(iouReport) && !isSettlementOrApprovalPartial) && isOnHold); /* Show the merchant for IOUs and expenses only if: From 887637426a1f07640aa495a6f926154e39659037 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Tue, 9 Apr 2024 17:56:00 +0200 Subject: [PATCH 057/580] fix: wrap the page for displaying offline indicator correctly, do not redirect back to the wallet --- .../AddBankAccount/AddBankAccount.tsx | 36 ++++++------- .../AddBankAccount/SetupMethod.tsx | 50 ++++++++++--------- 2 files changed, 45 insertions(+), 41 deletions(-) diff --git a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx index c07ad9aba587..2e54a892097c 100644 --- a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx +++ b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx @@ -64,7 +64,7 @@ function AddBankAccount({personalBankAccount, plaidData, personalBankAccountDraf PaymentMethods.continueSetup(onSuccessFallbackRoute); return; } - Navigation.goBack(ROUTES.SETTINGS_WALLET); + Navigation.goBack(); }; const handleBackButtonPress = () => { @@ -92,23 +92,25 @@ function AddBankAccount({personalBankAccount, plaidData, personalBankAccountDraf onBackButtonPress={handleBackButtonPress} title={translate('bankAccount.addBankAccount')} /> - {isSetupTypeChosen ? ( - <> - <View style={[styles.ph5, styles.mb5, styles.mt3, {height: CONST.BANK_ACCOUNT.STEPS_HEADER_HEIGHT}]}> - <InteractiveStepSubHeader - startStepIndex={0} - stepNames={CONST.WALLET.STEP_NAMES} + <View style={styles.flex1}> + {isSetupTypeChosen ? ( + <> + <View style={[styles.ph5, styles.mb5, styles.mt3, {height: CONST.BANK_ACCOUNT.STEPS_HEADER_HEIGHT}]}> + <InteractiveStepSubHeader + startStepIndex={0} + stepNames={CONST.WALLET.STEP_NAMES} + /> + </View> + <SubStep + isEditing={isEditing} + onNext={nextScreen} + onMove={moveTo} /> - </View> - <SubStep - isEditing={isEditing} - onNext={nextScreen} - onMove={moveTo} - /> - </> - ) : ( - <SetupMethod /> - )} + </> + ) : ( + <SetupMethod /> + )} + </View> </ScreenWrapper> ); } diff --git a/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx b/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx index c03e983d5c4f..e70d5979e92e 100644 --- a/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx +++ b/src/pages/EnablePayments/AddBankAccount/SetupMethod.tsx @@ -35,31 +35,33 @@ function SetupMethod({isPlaidDisabled, user}: SetupMethodProps) { const {translate} = useLocalize(); return ( - <Section - icon={Illustrations.MoneyWings} - title={translate('walletPage.addYourBankAccount')} - titleStyles={[styles.textHeadlineLineHeightXXL]} - > - <View style={[styles.mv3]}> - <Text>{translate('walletPage.addBankAccountBody')}</Text> - </View> - {!!plaidDesktopMessage && ( - <View style={[styles.mv3, styles.flexRow, styles.justifyContentBetween]}> - <TextLink href={bankAccountRoute}>{translate(plaidDesktopMessage)}</TextLink> + <View> + <Section + icon={Illustrations.MoneyWings} + title={translate('walletPage.addYourBankAccount')} + titleStyles={[styles.textHeadlineLineHeightXXL]} + > + <View style={[styles.mv3]}> + <Text>{translate('walletPage.addBankAccountBody')}</Text> </View> - )} - <Button - icon={Expensicons.Bank} - text={translate('bankAccount.addBankAccount')} - onPress={() => BankAccounts.openPersonalBankAccountSetupViewRefactor()} - isDisabled={isPlaidDisabled ?? !user?.validated} - style={[styles.mt4, styles.mb2]} - iconStyles={styles.buttonCTAIcon} - shouldShowRightIcon - success - large - /> - </Section> + {!!plaidDesktopMessage && ( + <View style={[styles.mv3, styles.flexRow, styles.justifyContentBetween]}> + <TextLink href={bankAccountRoute}>{translate(plaidDesktopMessage)}</TextLink> + </View> + )} + <Button + icon={Expensicons.Bank} + text={translate('bankAccount.addBankAccount')} + onPress={() => BankAccounts.openPersonalBankAccountSetupViewRefactor()} + isDisabled={isPlaidDisabled ?? !user?.validated} + style={[styles.mt4, styles.mb2]} + iconStyles={styles.buttonCTAIcon} + shouldShowRightIcon + success + large + /> + </Section> + </View> ); } From 53e97e327b5d34bb564745043063cd878cec772b Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:10:54 +0530 Subject: [PATCH 058/580] Use periods at the end of error messages --- package-lock.json | 6 +- package.json | 2 +- src/languages/en.ts | 142 ++++++++++++++++++------------------ src/languages/es.ts | 170 ++++++++++++++++++++++---------------------- 4 files changed, 161 insertions(+), 159 deletions(-) diff --git a/package-lock.json b/package-lock.json index d61279efb85b..834f3a225ac7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -206,7 +206,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.44", + "eslint-config-expensify": "^2.0.46", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -26606,7 +26606,9 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.44", + "version": "2.0.46", + "resolved": "https://js-registry.sharechat.com/eslint-config-expensify/-/eslint-config-expensify-2.0.46.tgz", + "integrity": "sha512-yRJ1GmIKTN0e0x1bCHbefzvLVlQJeM1Xv7zXuRfY1nll1s0F4f6HI6vs/kMpveWn/KzcKhBmrNqASGslhRUb3A==", "dev": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 0e988576aaa4..13f4c394d2d7 100644 --- a/package.json +++ b/package.json @@ -257,7 +257,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.44", + "eslint-config-expensify": "^2.0.46", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", diff --git a/src/languages/en.ts b/src/languages/en.ts index 3b670f7b6ebc..15fa4b9df13e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -222,20 +222,20 @@ export default { conjunctionAt: 'at', genericErrorMessage: 'Oops... something went wrong and your request could not be completed. Please try again later.', error: { - invalidAmount: 'Invalid amount', - acceptTerms: 'You must accept the Terms of Service to continue', + invalidAmount: 'Invalid amount.', + acceptTerms: 'You must accept the Terms of Service to continue.', phoneNumber: `Please enter a valid phone number, with the country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER})`, fieldRequired: 'This field is required.', requestModified: 'This request is being modified by another member.', characterLimit: ({limit}: CharacterLimitParams) => `Exceeds the maximum length of ${limit} characters`, characterLimitExceedCounter: ({length, limit}) => `Character limit exceeded (${length}/${limit})`, - dateInvalid: 'Please select a valid date', + dateInvalid: 'Please select a valid date.', invalidDateShouldBeFuture: 'Please choose today or a future date.', invalidTimeShouldBeFuture: 'Please choose a time at least one minute ahead.', - invalidCharacter: 'Invalid character', - enterMerchant: 'Enter a merchant name', - enterAmount: 'Enter an amount', - enterDate: 'Enter a date', + invalidCharacter: 'Invalid character.', + enterMerchant: 'Enter a merchant name.', + enterAmount: 'Enter an amount.', + enterDate: 'Enter a date.', }, comma: 'comma', semicolon: 'semicolon', @@ -695,17 +695,17 @@ export default { invalidCategoryLength: 'The length of the category chosen exceeds the maximum allowed (255). Please choose a different or shorten the category name first.', invalidAmount: 'Please enter a valid amount before continuing.', invalidTaxAmount: ({amount}: RequestAmountParams) => `Maximum tax amount is ${amount}`, - invalidSplit: 'Split amounts do not equal total amount', - other: 'Unexpected error, please try again later', - genericCreateFailureMessage: 'Unexpected error requesting money, please try again later', - receiptFailureMessage: "The receipt didn't upload. ", - saveFileMessage: 'Download the file ', - loseFileMessage: 'or dismiss this error and lose it', - genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later', - genericEditFailureMessage: 'Unexpected error editing the money request, please try again later', - genericSmartscanFailureMessage: 'Transaction is missing fields', - duplicateWaypointsErrorMessage: 'Please remove duplicate waypoints', - atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses', + invalidSplit: 'Split amounts do not equal total amount.', + other: 'Unexpected error, please try again later.', + genericCreateFailureMessage: 'Unexpected error requesting money, please try again later.', + receiptFailureMessage: "The receipt didn't upload.", + saveFileMessage: 'Download the file .', + loseFileMessage: 'or dismiss this error and lose it.', + genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later.', + genericEditFailureMessage: 'Unexpected error editing the money request, please try again later.', + genericSmartscanFailureMessage: 'Transaction is missing fields.', + duplicateWaypointsErrorMessage: 'Please remove duplicate waypoints.', + atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses.', splitBillMultipleParticipantsErrorMessage: 'Split bill is only allowed between a single workspace or individual users. Please update your selection.', invalidMerchant: 'Please enter a correct merchant.', }, @@ -942,9 +942,9 @@ export default { currentPassword: 'Current password', newPassword: 'New password', newPasswordPrompt: 'New password must be different than your old password, have at least 8 characters, 1 capital letter, 1 lowercase letter, and 1 number.', - errors: { - currentPassword: 'Current password is required', - newPasswordSameAsOld: 'New password must be different than your old password', + error: { + currentPassword: 'Current password is required.', + newPasswordSameAsOld: 'New password must be different than your old password.', newPassword: 'Your password must have at least 8 characters, 1 capital letter, 1 lowercase letter, and 1 number.', }, }, @@ -974,7 +974,7 @@ export default { }, recoveryCodeForm: { error: { - pleaseFillRecoveryCode: 'Please enter your recovery code', + pleaseFillRecoveryCode: 'Please enter your recovery code.', incorrectRecoveryCode: 'Incorrect recovery code. Please try again.', }, useRecoveryCode: 'Use recovery code', @@ -983,7 +983,7 @@ export default { }, twoFactorAuthForm: { error: { - pleaseFillTwoFactorAuth: 'Please enter your two-factor authentication code', + pleaseFillTwoFactorAuth: 'Please enter your two-factor authentication code.', incorrect2fa: 'Incorrect two-factor authentication code. Please try again.', }, }, @@ -998,7 +998,7 @@ export default { composerLabel: 'Notes', myNote: 'My note', error: { - genericFailureMessage: "Private notes couldn't be saved", + genericFailureMessage: "Private notes couldn't be saved.", }, }, addDebitCardPage: { @@ -1013,15 +1013,15 @@ export default { expensifyPassword: 'Expensify password', error: { invalidName: 'Name can only include letters.', - addressZipCode: 'Please enter a valid zip code', - debitCardNumber: 'Please enter a valid debit card number', - expirationDate: 'Please select a valid expiration date', - securityCode: 'Please enter a valid security code', - addressStreet: 'Please enter a valid billing address that is not a PO Box', - addressState: 'Please select a state', - addressCity: 'Please enter a city', - genericFailureMessage: 'An error occurred while adding your card, please try again', - password: 'Please enter your Expensify password', + addressZipCode: 'Please enter a valid zip code.', + debitCardNumber: 'Please enter a valid debit card number.', + expirationDate: 'Please select a valid expiration date.', + securityCode: 'Please enter a valid security code.', + addressStreet: 'Please enter a valid billing address that is not a PO Box.', + addressState: 'Please select a state.', + addressCity: 'Please enter a city.', + genericFailureMessage: 'An error occurred while adding your card, please try again.', + password: 'Please enter your Expensify password.', }, }, walletPage: { @@ -1281,9 +1281,9 @@ export default { requestNewCode: 'Request a new code in ', requestNewCodeAfterErrorOccurred: 'Request a new code', error: { - pleaseFillMagicCode: 'Please enter your magic code', + pleaseFillMagicCode: 'Please enter your magic code.', incorrectMagicCode: 'Incorrect magic code.', - pleaseFillTwoFactorAuth: 'Please enter your two-factor authentication code', + pleaseFillTwoFactorAuth: 'Please enter your two-factor authentication code.', }, }, passwordForm: { @@ -1334,14 +1334,14 @@ export default { [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: "I'm just looking around", }, error: { - requiredFirstName: 'Please input your first name to continue', - requiredLastName: 'Please input your last name to continue', + requiredFirstName: 'Please input your first name to continue.', + requiredLastName: 'Please input your last name to continue.', }, }, personalDetails: { error: { - containsReservedWord: 'Name cannot contain the words Expensify or Concierge', - hasInvalidCharacter: 'Name cannot contain a comma or semicolon', + containsReservedWord: 'Name cannot contain the words Expensify or Concierge.', + hasInvalidCharacter: 'Name cannot contain a comma or semicolon.', }, }, privatePersonalDetails: { @@ -1479,32 +1479,32 @@ export default { hasCurrencyError: 'Oops! It appears that your workspace currency is set to a different currency than USD. To proceed, please set it to USD and try again', error: { youNeedToSelectAnOption: 'You need to select an option to proceed.', - noBankAccountAvailable: 'Sorry, no bank account is available', - noBankAccountSelected: 'Please choose an account', - taxID: 'Please enter a valid tax ID number', + noBankAccountAvailable: 'Sorry, no bank account is available.', + noBankAccountSelected: 'Please choose an account.', + taxID: 'Please enter a valid tax ID number.', website: 'Please enter a valid website. The website should be in lowercase.', zipCode: `Incorrect zip code format. Acceptable format: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, - phoneNumber: 'Please enter a valid phone number', - companyName: 'Please enter a valid legal business name', - addressCity: 'Please enter a valid city', - addressStreet: 'Please enter a valid street address that is not a PO box', - addressState: 'Please select a valid state', - incorporationDateFuture: 'Incorporation date cannot be in the future', - incorporationState: 'Please select a valid state', + phoneNumber: 'Please enter a valid phone number.', + companyName: 'Please enter a valid legal business name.', + addressCity: 'Please enter a valid city.', + addressStreet: 'Please enter a valid street address that is not a PO box.', + addressState: 'Please select a valid state.', + incorporationDateFuture: 'Incorporation date cannot be in the future.', + incorporationState: 'Please select a valid state.', industryCode: 'Please enter a valid industry classification code. Must be 6 digits.', - restrictedBusiness: 'Please confirm company is not on the list of restricted businesses', - routingNumber: 'Please enter a valid routing number', - accountNumber: 'Please enter a valid account number', - routingAndAccountNumberCannotBeSame: 'The routing number and account number cannot be the same', - companyType: 'Please select a valid company type', + restrictedBusiness: 'Please confirm company is not on the list of restricted businesses.', + routingNumber: 'Please enter a valid routing number.', + accountNumber: 'Please enter a valid account number.', + routingAndAccountNumberCannotBeSame: 'The routing number and account number cannot be the same.', + companyType: 'Please select a valid company type.', tooManyAttempts: 'Due to a high number of login attempts, this option has been temporarily disabled for 24 hours. Please try again later or manually enter details instead.', - address: 'Please enter a valid address', - dob: 'Please select a valid date of birth', - age: 'Must be over 18 years old', - ssnLast4: 'Please enter valid last 4 digits of SSN', - firstName: 'Please enter a valid first name', - lastName: 'Please enter a valid last name', - noDefaultDepositAccountOrDebitCardAvailable: 'Please add a default deposit bank account or debit card', + address: 'Please enter a valid address.', + dob: 'Please select a valid date of birth.', + age: 'Must be over 18 years old.', + ssnLast4: 'Please enter valid last 4 digits of SSN.', + firstName: 'Please enter a valid first name.', + lastName: 'Please enter a valid last name.', + noDefaultDepositAccountOrDebitCardAvailable: 'Please add a default deposit bank account or debit card.', validationAmounts: 'The validation amounts you entered are incorrect. Please double-check your bank statement and try again.', }, }, @@ -1767,7 +1767,7 @@ export default { termsAndConditions: 'terms and conditions', certifyTrueAndAccurate: 'I certify that the information provided is true and accurate', error: { - certify: 'Must certify information is true and accurate', + certify: 'Must certify information is true and accurate.', }, }, completeVerificationStep: { @@ -1980,7 +1980,7 @@ export default { foreignDefault: 'Foreign currency default', customTaxName: 'Custom tax name', value: 'Value', - errors: { + error: { taxRateAlreadyExists: 'This tax name is already in use.', valuePercentageRange: 'Please enter a valid percentage between 0 and 100.', customNameRequired: 'Custom tax name is required.', @@ -2449,8 +2449,8 @@ export default { okay: 'Okay', }, error: { - title: 'Update Check Failed', - message: "We couldn't look for an update. Please check again in a bit!", + title: 'Update Check Failed.', + message: "We couldn't look for an update. Please check again in a bit!.", }, }, report: { @@ -2576,10 +2576,10 @@ export default { contactMethods: 'Contact methods.', schoolMailAsDefault: 'Before you move forward, please make sure to set your school email as your default contact method. You can do so in Settings > Profile > ', error: { - enterPhoneEmail: 'Enter a valid email or phone number', - enterEmail: 'Enter an email', - enterValidEmail: 'Enter a valid email', - tryDifferentEmail: 'Please try a different email', + enterPhoneEmail: 'Enter a valid email or phone number.', + enterEmail: 'Enter an email.', + enterValidEmail: 'Enter a valid email.', + tryDifferentEmail: 'Please try a different email.', }, }, cardTransactions: { @@ -2601,8 +2601,8 @@ export default { subtitle: 'The map will be generated when you go back online', onlineSubtitle: 'One moment while we set up the map', }, - errors: { - selectSuggestedAddress: 'Please select a suggested address or use current location', + error: { + selectSuggestedAddress: 'Please select a suggested address or use current location.', }, }, reportCardLostOrDamaged: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 5027174b2922..d9ca5932aeff 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -212,20 +212,20 @@ export default { conjunctionAt: 'a', genericErrorMessage: 'Ups... algo no ha ido bien y la acción no se ha podido completar. Por favor, inténtalo más tarde.', error: { - invalidAmount: 'Importe no válido', - acceptTerms: 'Debes aceptar los Términos de Servicio para continuar', + invalidAmount: 'Importe no válido.', + acceptTerms: 'Debes aceptar los Términos de Servicio para continuar.', phoneNumber: `Introduce un teléfono válido, incluyendo el código del país (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER})`, fieldRequired: 'Este campo es obligatorio.', requestModified: 'Esta solicitud está siendo modificada por otro miembro.', characterLimit: ({limit}: CharacterLimitParams) => `Supera el límite de ${limit} caracteres`, characterLimitExceedCounter: ({length, limit}) => `Se superó el límite de caracteres (${length}/${limit})`, - dateInvalid: 'Por favor, selecciona una fecha válida', - invalidDateShouldBeFuture: 'Por favor, elige una fecha igual o posterior a hoy', - invalidTimeShouldBeFuture: 'Por favor, elige una hora al menos un minuto en el futuro', - invalidCharacter: 'Carácter invalido', - enterMerchant: 'Introduce un comerciante', - enterAmount: 'Introduce un importe', - enterDate: 'Introduce una fecha', + dateInvalid: 'Por favor, selecciona una fecha válida.', + invalidDateShouldBeFuture: 'Por favor, elige una fecha igual o posterior a hoy.', + invalidTimeShouldBeFuture: 'Por favor, elige una hora al menos un minuto en el futuro.', + invalidCharacter: 'Carácter invalido.', + enterMerchant: 'Introduce un comerciante.', + enterAmount: 'Introduce un importe.', + enterDate: 'Introduce una fecha.', }, comma: 'la coma', semicolon: 'el punto y coma', @@ -693,17 +693,17 @@ export default { invalidCategoryLength: 'El largo de la categoría escogida excede el máximo permitido (255). Por favor, escoge otra categoría o acorta la categoría primero.', invalidAmount: 'Por favor, ingresa un importe válido antes de continuar.', invalidTaxAmount: ({amount}: RequestAmountParams) => `El importe máximo del impuesto es ${amount}`, - invalidSplit: 'La suma de las partes no equivale al importe total', - other: 'Error inesperado, por favor inténtalo más tarde', - genericCreateFailureMessage: 'Error inesperado solicitando dinero. Por favor, inténtalo más tarde', - receiptFailureMessage: 'El recibo no se subió. ', - saveFileMessage: 'Guarda el archivo ', - loseFileMessage: 'o descarta este error y piérdelo', - genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde', - genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde', - genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', - duplicateWaypointsErrorMessage: 'Por favor, elimina los puntos de ruta duplicados', - atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes', + invalidSplit: 'La suma de las partes no equivale al importe total.', + other: 'Error inesperado, por favor inténtalo más tarde.', + genericCreateFailureMessage: 'Error inesperado solicitando dinero. Por favor, inténtalo más tarde.', + receiptFailureMessage: 'El recibo no se subió. .', + saveFileMessage: 'Guarda el archivo .', + loseFileMessage: 'o descarta este error y piérdelo.', + genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde.', + genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde.', + genericSmartscanFailureMessage: 'La transacción tiene campos vacíos.', + duplicateWaypointsErrorMessage: 'Por favor, elimina los puntos de ruta duplicados.', + atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes.', splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.', invalidMerchant: 'Por favor, introduce un comerciante correcto.', }, @@ -940,9 +940,9 @@ export default { currentPassword: 'Contraseña actual', newPassword: 'Nueva contraseña', newPasswordPrompt: 'La nueva contraseña debe ser diferente de la antigua, tener al menos 8 caracteres, 1 letra mayúscula, 1 letra minúscula y 1 número.', - errors: { - currentPassword: 'Contraseña actual es requerido', - newPasswordSameAsOld: 'La nueva contraseña tiene que ser diferente de la antigua', + error: { + currentPassword: 'Contraseña actual es requerido.', + newPasswordSameAsOld: 'La nueva contraseña tiene que ser diferente de la antigua.', newPassword: 'Su contraseña debe tener al menos 8 caracteres, 1 letra mayúscula, 1 letra minúscula y 1 número.', }, }, @@ -973,8 +973,8 @@ export default { }, recoveryCodeForm: { error: { - pleaseFillRecoveryCode: 'Por favor, introduce tu código de recuperación', - incorrectRecoveryCode: 'Código de recuperación incorrecto. Por favor, inténtalo de nuevo', + pleaseFillRecoveryCode: 'Por favor, introduce tu código de recuperación.', + incorrectRecoveryCode: 'Código de recuperación incorrecto. Por favor, inténtalo de nuevo.', }, useRecoveryCode: 'Usar código de recuperación', recoveryCode: 'Código de recuperación', @@ -982,8 +982,8 @@ export default { }, twoFactorAuthForm: { error: { - pleaseFillTwoFactorAuth: 'Por favor, introduce tu código de autenticación de dos factores', - incorrect2fa: 'Código de autenticación de dos factores incorrecto. Por favor, inténtalo de nuevo', + pleaseFillTwoFactorAuth: 'Por favor, introduce tu código de autenticación de dos factores.', + incorrect2fa: 'Código de autenticación de dos factores incorrecto. Por favor, inténtalo de nuevo.', }, }, passwordConfirmationScreen: { @@ -997,7 +997,7 @@ export default { composerLabel: 'Notas', myNote: 'Mi nota', error: { - genericFailureMessage: 'Las notas privadas no han podido ser guardadas', + genericFailureMessage: 'Las notas privadas no han podido ser guardadas.', }, }, addDebitCardPage: { @@ -1012,15 +1012,15 @@ export default { expensifyPassword: 'Contraseña de Expensify', error: { invalidName: 'El nombre sólo puede incluir letras.', - addressZipCode: 'Por favor, introduce un código postal válido', - debitCardNumber: 'Por favor, introduce un número de tarjeta de débito válido', - expirationDate: 'Por favor, selecciona una fecha de vencimiento válida', - securityCode: 'Por favor, introduce un código de seguridad válido', - addressStreet: 'Por favor, introduce una dirección de facturación válida que no sea un apartado postal', - addressState: 'Por favor, selecciona un estado', - addressCity: 'Por favor, introduce una ciudad', - genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Vuelva a intentarlo', - password: 'Por favor, introduce tu contraseña de Expensify', + addressZipCode: 'Por favor, introduce un código postal válido.', + debitCardNumber: 'Por favor, introduce un número de tarjeta de débito válido.', + expirationDate: 'Por favor, selecciona una fecha de vencimiento válida.', + securityCode: 'Por favor, introduce un código de seguridad válido.', + addressStreet: 'Por favor, introduce una dirección de facturación válida que no sea un apartado postal.', + addressState: 'Por favor, selecciona un estado.', + addressCity: 'Por favor, introduce una ciudad.', + genericFailureMessage: 'Se produjo un error al añadir tu tarjeta. Vuelva a intentarlo.', + password: 'Por favor, introduce tu contraseña de Expensify.', }, }, walletPage: { @@ -1283,9 +1283,9 @@ export default { requestNewCode: 'Pedir un código nuevo en ', requestNewCodeAfterErrorOccurred: 'Solicitar un nuevo código', error: { - pleaseFillMagicCode: 'Por favor, introduce el código mágico', + pleaseFillMagicCode: 'Por favor, introduce el código mágico.', incorrectMagicCode: 'Código mágico incorrecto.', - pleaseFillTwoFactorAuth: 'Por favor, introduce tu código de autenticación de dos factores', + pleaseFillTwoFactorAuth: 'Por favor, introduce tu código de autenticación de dos factores.', }, }, passwordForm: { @@ -1297,15 +1297,15 @@ export default { requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado', error: { incorrectPassword: 'Contraseña incorrecta. Por favor, inténtalo de nuevo.', - incorrectLoginOrPassword: 'Usuario o contraseña incorrectos. Por favor, inténtalo de nuevo', - incorrect2fa: 'Código de autenticación de dos factores incorrecto. Por favor, inténtalo de nuevo', - twoFactorAuthenticationEnabled: 'Tienes autenticación de 2 factores activada en esta cuenta. Por favor, conéctate usando tu email o número de teléfono', - invalidLoginOrPassword: 'Usuario o clave incorrectos. Por favor, inténtalo de nuevo o restablece la contraseña', + incorrectLoginOrPassword: 'Usuario o contraseña incorrectos. Por favor, inténtalo de nuevo.', + incorrect2fa: 'Código de autenticación de dos factores incorrecto. Por favor, inténtalo de nuevo.', + twoFactorAuthenticationEnabled: 'Tienes autenticación de 2 factores activada en esta cuenta. Por favor, conéctate usando tu email o número de teléfono.', + invalidLoginOrPassword: 'Usuario o clave incorrectos. Por favor, inténtalo de nuevo o restablece la contraseña.', unableToResetPassword: - 'No se pudo cambiar tu clave. Probablemente porque el enlace para restablecer la contrasenña ha expirado. Te hemos enviado un nuevo enlace. Comprueba tu bandeja de entrada y carpeta de Spam', - noAccess: 'No tienes acceso a esta aplicación. Por favor, añade tu usuario de GitHub para acceder', - accountLocked: 'Tu cuenta ha sido bloqueada tras varios intentos fallidos. Por favor, inténtalo de nuevo dentro de una hora', - fallback: 'Ha ocurrido un error. Por favor, inténtalo mas tarde', + 'No se pudo cambiar tu clave. Probablemente porque el enlace para restablecer la contrasenña ha expirado. Te hemos enviado un nuevo enlace. Comprueba tu bandeja de entrada y carpeta de Spam.', + noAccess: 'No tienes acceso a esta aplicación. Por favor, añade tu usuario de GitHub para acceder.', + accountLocked: 'Tu cuenta ha sido bloqueada tras varios intentos fallidos. Por favor, inténtalo de nuevo dentro de una hora.', + fallback: 'Ha ocurrido un error. Por favor, inténtalo mas tarde.', }, }, loginForm: { @@ -1336,14 +1336,14 @@ export default { [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: 'Sólo estoy mirando', }, error: { - requiredFirstName: 'Introduce tu nombre para continuar', - requiredLastName: 'Introduce tu apellido para continuar', + requiredFirstName: 'Introduce tu nombre para continuar.', + requiredLastName: 'Introduce tu apellido para continuar.', }, }, personalDetails: { error: { - containsReservedWord: 'El nombre no puede contener las palabras Expensify o Concierge', - hasInvalidCharacter: 'El nombre no puede contener una coma o un punto y coma', + containsReservedWord: 'El nombre no puede contener las palabras Expensify o Concierge.', + hasInvalidCharacter: 'El nombre no puede contener una coma o un punto y coma.', }, }, privatePersonalDetails: { @@ -1500,33 +1500,33 @@ export default { '¡Ups! Parece que la moneda de tu espacio de trabajo está configurada en una moneda diferente a USD. Para continuar, por favor configúrala en USD e inténtalo nuevamente.', error: { youNeedToSelectAnOption: 'Debes seleccionar una opción para continuar.', - noBankAccountAvailable: 'Lo sentimos, no hay ninguna cuenta bancaria disponible', - noBankAccountSelected: 'Por favor, elige una cuenta bancaria', - taxID: 'Por favor, introduce un número de identificación fiscal válido', + noBankAccountAvailable: 'Lo sentimos, no hay ninguna cuenta bancaria disponible.', + noBankAccountSelected: 'Por favor, elige una cuenta bancaria.', + taxID: 'Por favor, introduce un número de identificación fiscal válido.', website: 'Por favor, introduce un sitio web válido. El sitio web debe estar en minúsculas.', zipCode: `Formato de código postal incorrecto. Formato aceptable: ${CONST.COUNTRY_ZIP_REGEX_DATA.US.samples}`, - phoneNumber: 'Por favor, introduce un teléfono válido', - companyName: 'Por favor, introduce un nombre comercial legal válido', - addressCity: 'Por favor, introduce una ciudad válida', - addressStreet: 'Por favor, introduce una calle de dirección válida que no sea un apartado postal', - addressState: 'Por favor, selecciona un estado', - incorporationDateFuture: 'La fecha de incorporación no puede ser futura', - incorporationState: 'Por favor, selecciona una estado válido', - industryCode: 'Por favor, introduce un código de clasificación de industria válido', - restrictedBusiness: 'Por favor, confirma que la empresa no está en la lista de negocios restringidos', - routingNumber: 'Por favor, introduce un número de ruta válido', - accountNumber: 'Por favor, introduce un número de cuenta válido', - routingAndAccountNumberCannotBeSame: 'El número de ruta y el número de cuenta no pueden ser iguales', - companyType: 'Por favor, selecciona un tipo de compañía válido', + phoneNumber: 'Por favor, introduce un teléfono válido.', + companyName: 'Por favor, introduce un nombre comercial legal válido.', + addressCity: 'Por favor, introduce una ciudad válida.', + addressStreet: 'Por favor, introduce una calle de dirección válida que no sea un apartado postal.', + addressState: 'Por favor, selecciona un estado.', + incorporationDateFuture: 'La fecha de incorporación no puede ser futura.', + incorporationState: 'Por favor, selecciona una estado válido.', + industryCode: 'Por favor, introduce un código de clasificación de industria válido.', + restrictedBusiness: 'Por favor, confirma que la empresa no está en la lista de negocios restringidos.', + routingNumber: 'Por favor, introduce un número de ruta válido.', + accountNumber: 'Por favor, introduce un número de cuenta válido.', + routingAndAccountNumberCannotBeSame: 'El número de ruta y el número de cuenta no pueden ser iguales.', + companyType: 'Por favor, selecciona un tipo de compañía válido.', tooManyAttempts: 'Debido a la gran cantidad de intentos de inicio de sesión, esta opción ha sido desactivada temporalmente durante 24 horas. Por favor, inténtalo de nuevo más tarde.', - address: 'Por favor, introduce una dirección válida', - dob: 'Por favor, selecciona una fecha de nacimiento válida', - age: 'Debe ser mayor de 18 años', - ssnLast4: 'Por favor, introduce los últimos 4 dígitos del número de seguridad social', - firstName: 'Por favor, introduce el nombre', - lastName: 'Por favor, introduce los apellidos', - noDefaultDepositAccountOrDebitCardAvailable: 'Por favor, añade una cuenta bancaria para depósitos o una tarjeta de débito', + address: 'Por favor, introduce una dirección válida.', + dob: 'Por favor, selecciona una fecha de nacimiento válida.', + age: 'Debe ser mayor de 18 años.', + ssnLast4: 'Por favor, introduce los últimos 4 dígitos del número de seguridad social.', + firstName: 'Por favor, introduce el nombre.', + lastName: 'Por favor, introduce los apellidos.', + noDefaultDepositAccountOrDebitCardAvailable: 'Por favor, añade una cuenta bancaria para depósitos o una tarjeta de débito.', validationAmounts: 'Los importes de validación que introduciste son incorrectos. Por favor, comprueba tu cuenta bancaria e inténtalo de nuevo.', }, }, @@ -1793,7 +1793,7 @@ export default { termsAndConditions: 'Términos y condiciones', certifyTrueAndAccurate: 'Certifico que la información dada es correcta', error: { - certify: 'Debe certificar que la información es verdadera y precisa', + certify: 'Debe certificar que la información es verdadera y precisa.', }, }, completeVerificationStep: { @@ -2007,10 +2007,10 @@ export default { foreignDefault: 'Moneda extranjera por defecto', customTaxName: 'Nombre del impuesto', value: 'Valor', - errors: { - taxRateAlreadyExists: 'Ya existe un impuesto con este nombre', + error: { + taxRateAlreadyExists: 'Ya existe un impuesto con este nombre.', customNameRequired: 'El nombre del impuesto es obligatorio.', - valuePercentageRange: 'Por favor, introduce un porcentaje entre 0 y 100', + valuePercentageRange: 'Por favor, introduce un porcentaje entre 0 y 100.', deleteFailureMessage: 'Se ha producido un error al intentar eliminar la tasa de impuesto. Por favor, inténtalo más tarde.', updateFailureMessage: 'Se ha producido un error al intentar modificar la tasa de impuesto. Por favor, inténtalo más tarde.', createFailureMessage: 'Se ha producido un error al intentar crear la tasa de impuesto. Por favor, inténtalo más tarde.', @@ -2481,8 +2481,8 @@ export default { okay: 'Vale', }, error: { - title: 'Comprobación fallida', - message: 'No hemos podido comprobar si existe una actualización. ¡Inténtalo de nuevo más tarde!', + title: 'Comprobación fallida.', + message: 'No hemos podido comprobar si existe una actualización. ¡Inténtalo de nuevo más tarde!.', }, }, report: { @@ -3069,10 +3069,10 @@ export default { schoolMailAsDefault: 'Antes de seguir adelante, asegúrate de establecer el correo electrónico de tu colegio como método de contacto predeterminado. Puede hacerlo en Configuración > Perfil > ', error: { - enterPhoneEmail: 'Ingrese un correo electrónico o número de teléfono válido', - enterEmail: 'Introduce un correo electrónico', - enterValidEmail: 'Introduzca un correo electrónico válido', - tryDifferentEmail: 'Por favor intenta con un e-mail diferente', + enterPhoneEmail: 'Ingrese un correo electrónico o número de teléfono válido.', + enterEmail: 'Introduce un correo electrónico.', + enterValidEmail: 'Introduzca un correo electrónico válido.', + tryDifferentEmail: 'Por favor intenta con un e-mail diferente.', }, }, cardTransactions: { @@ -3094,7 +3094,7 @@ export default { subtitle: 'El mapa se generará cuando vuelvas a estar en línea', onlineSubtitle: 'Un momento mientras configuramos el mapa', }, - errors: { + error: { selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual.', }, }, From 4a3822ed03119b14a79fb125f00d5248618cbe7b Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 10 Apr 2024 12:15:22 +0530 Subject: [PATCH 059/580] Use periods at the end of error messages --- src/libs/actions/TaxRate.ts | 14 +++++++------- .../iou/request/step/IOURequestStepWaypoint.tsx | 4 ++-- .../taxes/WorkspaceTaxesSettingsCustomTaxName.tsx | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index ec69ea79b7ee..f8425cd0c40c 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -39,7 +39,7 @@ const validateTaxName = (policy: Policy, values: FormOnyxValues<typeof ONYXKEYS. const name = values[INPUT_IDS.NAME]; if (policy?.taxRates?.taxes && ValidationUtils.isExistingTaxName(name, policy.taxRates.taxes)) { - errors[INPUT_IDS.NAME] = 'workspace.taxes.errors.taxRateAlreadyExists'; + errors[INPUT_IDS.NAME] = 'workspace.taxes.error.taxRateAlreadyExists'; } return errors; @@ -53,7 +53,7 @@ const validateTaxValue = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WORKSPACE const value = values[INPUT_IDS.VALUE]; if (!ValidationUtils.isValidPercentage(value)) { - errors[INPUT_IDS.VALUE] = 'workspace.taxes.errors.valuePercentageRange'; + errors[INPUT_IDS.VALUE] = 'workspace.taxes.error.valuePercentageRange'; } return errors; @@ -123,7 +123,7 @@ function createPolicyTax(policyID: string, taxRate: TaxRate) { taxRates: { taxes: { [taxRate.code]: { - errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.createFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.error.createFailureMessage'), }, }, }, @@ -231,7 +231,7 @@ function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isE acc[taxID] = { isDisabled: !!originalTaxes[taxID].isDisabled, pendingFields: {isDisabled: null}, - errorFields: {isDisabled: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.updateFailureMessage')}, + errorFields: {isDisabled: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.error.updateFailureMessage')}, }; return acc; }, {}), @@ -312,7 +312,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { taxes: taxesToDelete.reduce<TaxRateDeleteMap>((acc, taxID) => { acc[taxID] = { pendingAction: null, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.deleteFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.error.deleteFailureMessage'), }; return acc; }, {}), @@ -376,7 +376,7 @@ function updatePolicyTaxValue(policyID: string, taxID: string, taxValue: number) [taxID]: { value: originalTaxRate.value, pendingFields: {value: null}, - errorFields: {value: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.updateFailureMessage')}, + errorFields: {value: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.error.updateFailureMessage')}, }, }, }, @@ -438,7 +438,7 @@ function renamePolicyTax(policyID: string, taxID: string, newName: string) { [taxID]: { name: originalTaxRate.name, pendingFields: {name: null}, - errorFields: {name: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.updateFailureMessage')}, + errorFields: {name: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.error.updateFailureMessage')}, }, }, }, diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index 93f9e03a7494..59cc794c2406 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -96,7 +96,7 @@ function IOURequestStepWaypoint({ // If the user is online, and they are trying to save a value without using the autocomplete, show an error message instructing them to use a selected address instead. // That enables us to save the address with coordinates when it is selected if (!isOffline && waypointValue !== '' && waypointAddress !== waypointValue) { - ErrorUtils.addErrorMessage(errors, `waypoint${pageIndex}`, 'distance.errors.selectSuggestedAddress'); + ErrorUtils.addErrorMessage(errors, `waypoint${pageIndex}`, 'distance.error.selectSuggestedAddress'); } return errors; @@ -204,7 +204,7 @@ function IOURequestStepWaypoint({ ref={(e: HTMLElement | null) => { textInput.current = e as unknown as TextInput; }} - hint={!isOffline ? 'distance.errors.selectSuggestedAddress' : ''} + hint={!isOffline ? 'distance.error.selectSuggestedAddress' : ''} containerStyles={[styles.mt4]} label={translate('distance.address')} defaultValue={waypointAddress} diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx index e9e359d9d059..4054e789af27 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx @@ -43,7 +43,7 @@ function WorkspaceTaxesSettingsCustomTaxName({ const customTaxName = values[INPUT_IDS.NAME]; if (!ValidationUtils.isRequiredFulfilled(customTaxName)) { - errors.name = 'workspace.taxes.errors.customNameRequired'; + errors.name = 'workspace.taxes.error.customNameRequired'; } return errors; From 7f4f51245d96b0184b717aa36883e21d23835397 Mon Sep 17 00:00:00 2001 From: bartektomczyk <bartlomiejtomczyk@gmail.com> Date: Thu, 21 Mar 2024 12:00:57 +0100 Subject: [PATCH 060/580] feat: added component for displaying static message inm onboarding --- src/languages/en.ts | 10 ++ src/languages/es.ts | 10 ++ .../report/OnboardingReportFooterMessage.tsx | 92 +++++++++++++++++++ src/styles/index.ts | 4 + 4 files changed, 116 insertions(+) create mode 100644 src/pages/home/report/OnboardingReportFooterMessage.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index 3b670f7b6ebc..138b28260060 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2665,6 +2665,16 @@ export default { welcomeMessage: 'Welcome to Expensify', welcomeSubtitle: 'What would you like to do?', }, + onboardingBottomMessage: { + [CONST.INTRO_CHOICES.MANAGE_TEAM]: { + phrase1: 'Chat with your setup specialist in ', + phrase2: ' for help', + }, + default: { + phrase1: 'Message ', + phrase2: ' for help with setup', + }, + }, manageTeams: { [CONST.MANAGE_TEAMS_CHOICE.MULTI_LEVEL]: 'Multi level approval', [CONST.MANAGE_TEAMS_CHOICE.CUSTOM_EXPENSE]: 'Custom expense coding', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5027174b2922..0799aefac37d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3158,6 +3158,16 @@ export default { welcomeMessage: 'Bienvenido a Expensify', welcomeSubtitle: '¿Qué te gustaría hacer?', }, + onboardingBottomMessage: { + [CONST.INTRO_CHOICES.MANAGE_TEAM]: { + phrase1: 'Chat with your setup specialist in ', + phrase2: ' for help', + }, + default: { + phrase1: 'Message ', + phrase2: ' for help with setup', + }, + }, manageTeams: { [CONST.MANAGE_TEAMS_CHOICE.MULTI_LEVEL]: 'Aprobación multinivel', [CONST.MANAGE_TEAMS_CHOICE.CUSTOM_EXPENSE]: 'Codificación personalizada de gastos', diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx new file mode 100644 index 000000000000..a48a609b51a7 --- /dev/null +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -0,0 +1,92 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import Navigation from '@navigation/Navigation'; +import * as ReportInstance from '@userActions/Report'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {Policy as PolicyType, Report} from '@src/types/onyx'; + +type OnboardingReportFooterMessageOnyxProps = {reports: OnyxCollection<Report>; policies: OnyxCollection<PolicyType>}; +type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps & {choice: ValueOf<typeof CONST.INTRO_CHOICES>}; + +function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingReportFooterMessageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + + const adminChatReport = useMemo(() => { + const adminsReports = reports ? Object.values(reports).filter((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS) : []; + const activePolicies = policies ? Object.values(policies).filter((policy) => PolicyUtils.shouldShowPolicy(policy, false)) : []; + + return adminsReports.find((report) => activePolicies.find((policy) => policy?.id === report?.policyID)); + }, [policies, reports]); + + const content = useMemo(() => { + switch (choice) { + case CONST.INTRO_CHOICES.MANAGE_TEAM: + return ( + <> + {`${translate('onboardingBottomMessage.newDotManageTeam.phrase1')}`} + <TextLink + style={styles.label} + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminChatReport?.reportID ?? ''))} + > + {adminChatReport?.reportName ?? CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS} + </TextLink> + {`${translate('onboardingBottomMessage.newDotManageTeam.phrase2')}`} + </> + ); + default: + return ( + <> + {`${translate('onboardingBottomMessage.default.phrase1')}`} + <TextLink + style={styles.label} + onPress={() => ReportInstance.navigateToConciergeChat()} + > + {`${CONST?.CONCIERGE_CHAT_NAME}`} + </TextLink> + {`${translate('onboardingBottomMessage.default.phrase2')}`} + </> + ); + } + }, [adminChatReport?.reportName, adminChatReport?.reportID, choice, styles.label, translate]); + + return ( + <View + style={[ + styles.chatFooter, + isSmallScreenWidth ? styles.mb5 : styles.mb4, + styles.mh5, + styles.flexRow, + styles.alignItemsCenter, + styles.p4, + styles.borderRadiusComponentLarge, + styles.hoveredComponentBG, + styles.breakWord, + styles.justifyContentCenter, + ]} + > + <Text style={[styles.textSupporting, styles.label]}>{content}</Text> + </View> + ); +} +OnboardingReportFooterMessage.displayName = 'OnboardingReportFooterMessage'; +export default withOnyx<OnboardingReportFooterMessageProps, OnboardingReportFooterMessageOnyxProps>({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(OnboardingReportFooterMessage); diff --git a/src/styles/index.ts b/src/styles/index.ts index f165974119ff..27178157f9f5 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -525,6 +525,10 @@ const styles = (theme: ThemeColors) => borderRadius: variables.buttonBorderRadius, }, + borderRadiusComponentLarge: { + borderRadius: variables.componentBorderRadiusLarge, + }, + bottomTabBarContainer: { flexDirection: 'row', height: variables.bottomTabHeight, From f76a57dd1e20a69ba54f17685826029ae25c4434 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:32:10 +0530 Subject: [PATCH 061/580] Update package-lock.json --- package-lock.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 834f3a225ac7..220b072a03ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26606,9 +26606,7 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.46", - "resolved": "https://js-registry.sharechat.com/eslint-config-expensify/-/eslint-config-expensify-2.0.46.tgz", - "integrity": "sha512-yRJ1GmIKTN0e0x1bCHbefzvLVlQJeM1Xv7zXuRfY1nll1s0F4f6HI6vs/kMpveWn/KzcKhBmrNqASGslhRUb3A==", + "version": "2.0.45", "dev": true, "license": "ISC", "dependencies": { From b11176ee988c051952a5cc21c91856222956407a Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:32:36 +0530 Subject: [PATCH 062/580] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 13f4c394d2d7..336254dddf7a 100644 --- a/package.json +++ b/package.json @@ -257,7 +257,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.46", + "eslint-config-expensify": "^2.0.45", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", From fa80eac51c7356e9c87a7464c69e52441ad91387 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:33:34 +0530 Subject: [PATCH 063/580] Update to use 2.0.45 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 220b072a03ce..f8ec2ca523a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -206,7 +206,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.46", + "eslint-config-expensify": "^2.0.45", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", From 981c45306e5029735bd9aa13ea2f4796fbcbdc2b Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:35:35 +0530 Subject: [PATCH 064/580] Update en.ts --- src/languages/en.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 15fa4b9df13e..99126e5fd8d3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -942,11 +942,6 @@ export default { currentPassword: 'Current password', newPassword: 'New password', newPasswordPrompt: 'New password must be different than your old password, have at least 8 characters, 1 capital letter, 1 lowercase letter, and 1 number.', - error: { - currentPassword: 'Current password is required.', - newPasswordSameAsOld: 'New password must be different than your old password.', - newPassword: 'Your password must have at least 8 characters, 1 capital letter, 1 lowercase letter, and 1 number.', - }, }, twoFactorAuth: { headerTitle: 'Two-factor authentication', From 36d3036fb8176cf561505d1ed1d2a849cbdcc39e Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:36:08 +0530 Subject: [PATCH 065/580] Update es.ts --- src/languages/es.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index d9ca5932aeff..cb9d9fbc8224 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -940,11 +940,6 @@ export default { currentPassword: 'Contraseña actual', newPassword: 'Nueva contraseña', newPasswordPrompt: 'La nueva contraseña debe ser diferente de la antigua, tener al menos 8 caracteres, 1 letra mayúscula, 1 letra minúscula y 1 número.', - error: { - currentPassword: 'Contraseña actual es requerido.', - newPasswordSameAsOld: 'La nueva contraseña tiene que ser diferente de la antigua.', - newPassword: 'Su contraseña debe tener al menos 8 caracteres, 1 letra mayúscula, 1 letra minúscula y 1 número.', - }, }, twoFactorAuth: { headerTitle: 'Autenticación de dos factores', From d6f54fe37801c6f2724d2e5f60265c99cfc5a90e Mon Sep 17 00:00:00 2001 From: Sonia Liapounova <sonia@expensify.com> Date: Wed, 10 Apr 2024 04:19:19 -0700 Subject: [PATCH 066/580] Delete docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md --- .../Global-Reimbursements.md | 106 ------------------ 1 file changed, 106 deletions(-) delete mode 100644 docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md diff --git a/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md b/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md deleted file mode 100644 index 2ff74760b376..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: International Reimbursements -description: International Reimbursements ---- -# Overview - -If your company’s business bank account is in the US, Canada, the UK, Europe, or Australia, you now have the option to send direct reimbursements to nearly any country across the globe! -The process to enable global reimbursements is dependent on the currency of your reimbursement bank account, so be sure to review the corresponding instructions below. - -# How to request international reimbursements - -## The reimbursement account is in USD - -If your reimbursement bank account is in USD, the first step is connecting the bank account to Expensify. -The individual who plans on sending reimbursements internationally should head to **Settings > Account > Payments > Add Verified Bank Account**. From there, you will provide company details, input personal information, and upload a copy of your ID. - -Once the USD bank account is verified (or if you already had a USD business bank account connected), click the support icon in your Expensify account to inform your Setup Specialist, Account Manager, or Concierge that you’d like to enable international reimbursements. From there, Expensify will ask you to confirm the currencies of the reimbursement and employee bank accounts. - -Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. - -## The reimbursement account is in AUD, CAD, GBP, EUR - -To request international reimbursements, contact Expensify Support to make that request. - -You can do this by clicking on the support icon and informing your Setup Specialist, Account Manager, or Concierge that you’d like to set up global reimbursements on your account. -From there, Expensify will ask you to confirm both the currencies of the reimbursement and employee bank accounts. - -Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. - -# How to verify the bank account for sending international payments - -Once international payments are enabled on your Expensify account, the next step is verifying the bank account to send the reimbursements. - -## The reimbursement account is in USD - -First, confirm the workspace settings are set up correctly by doing the following: -1. Head to **Settings > Workspaces > Group > _[Workspace Name]_ > Reports** and check that the workspace currency is USD -2. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the reimbursement method to direct -3. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the USD bank account to the default account - -Once that’s all set, head to **Settings > Account > Payments**, and click **Enable Global Reimbursement** on the bank account (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account). - -From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. - -## The reimbursement account is in AUD, CAD, GBP, EUR - -First, confirm the workspace currency corresponds with the currency of the reimbursement bank account. You can do this under **Settings > Workspaces > Group > _[Workspace Name]_ > Reports**. It should be AUD, CAD, GBP, or EUR. - -Next, add the bank account to Expensify: -1. Head to **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements** and set the reimbursement method to direct (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account) -2. Click **Add Business Bank Account** -3. If the incorrect country shows as the default, click **Switch Country** to select the correct country -4. Enter the bank account details -5. Click **Save & Continue** - -From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. - -# How to start reimbursing internationally - -After the bank account is verified for international payments, set the correct bank account as the reimbursement account. - -You can do this under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements** by selecting the reimbursement account as the default account. - -Finally, have your employees add their deposit-only bank accounts. They can do this by logging into their Expensify accounts, heading to **Settings > Account > Payments**, and clicking **Add Deposit-Only Bank Account**. - -# Deep Dive - -## Documents requested - -Our Compliance Team may ask for additional information depending on who initiates the verification or what information you provide on the DocuSign form. - -Examples of additional requested information: -- The reimburser’s proof of address and ID -- Company directors’ proofs of address and IDs -- An authorization letter -- An independently certified documentation such as shareholder agreement from a lawyer, notary, or public accountant if an individual owns more than 25% of the company - -{% include faq-begin.md %} - -## How many people can send reimbursements internationally? - -Once your company is authorized to send global payments, the individual who verified the bank account can share it with additional admins on the workspace. That way, multiple workspace members can send international reimbursements. - -## How long does it take to verify an account for international payments? - -It varies! The verification process can take a few business days to several weeks. It depends on whether or not the information in the DocuSign form is correct if our Compliance Team requires any additional information, and how responsive the employee verifying the company’s details is to our requests. - -## If I already have a USD bank account connected to Expensify, do I need to go through the verification process again to enable international payments? - -If you’ve already connected a US business bank account, you can request to enable global reimbursements by contacting Expensify Support immediately. However, additional steps are required to verify the bank account for international payments. - -## My employee says they don’t have the option to add their non-USD bank account as a deposit account – what should they do? - -Have the employee double-check that their default workspace is set as the workspace that's connected to the bank you're using to send international payments. - -An employee can confirm their default workspace is under **Settings > Workspaces > Group**. The default workspace has a green checkmark next to it. They can change their default workspace by clicking **Default Workspace** on the correct workspace. - -## Who is the “Authorized User” on the International Reimbursement DocuSign form? - -This is the person who will process international reimbursements. The authorized user should be the same person who manages the bank account connection in Expensify. - -## Who should I enter as the “User” on the International Reimbursement form? - -You can leave this form section blank since the “User” is Expensify. - -{% include faq-end.md %} From 820276d2ddae1c0f63e58376992e3a6c79688f4e Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Wed, 10 Apr 2024 17:03:06 +0200 Subject: [PATCH 067/580] Partly update start, participants and confirmation steps to support invoices --- assets/images/invoice-generic.svg | 4 ++ src/CONST.ts | 2 + src/components/Icon/Expensicons.ts | 2 + ...oraryForRefactorRequestConfirmationList.js | 44 ++++++++++++++++++- src/components/ReportWelcomeText.tsx | 6 ++- src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/IOUUtils.ts | 4 +- src/libs/PolicyUtils.ts | 16 ++++++- src/libs/actions/Policy.ts | 19 ++++++++ .../AttachmentPickerWithMenuItems.tsx | 5 +++ .../FloatingActionButtonAndPopover.tsx | 23 +++++++++- src/pages/iou/request/IOURequestStartPage.js | 9 ++-- ...yForRefactorRequestParticipantsSelector.js | 24 ++++++++-- .../step/IOURequestStepConfirmation.tsx | 8 ++++ .../step/IOURequestStepParticipants.js | 5 +++ 16 files changed, 162 insertions(+), 13 deletions(-) create mode 100644 assets/images/invoice-generic.svg diff --git a/assets/images/invoice-generic.svg b/assets/images/invoice-generic.svg new file mode 100644 index 000000000000..171b892fd8b3 --- /dev/null +++ b/assets/images/invoice-generic.svg @@ -0,0 +1,4 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M6.75 7C6.33579 7 6 7.33579 6 7.75C6 8.16421 6.33579 8.5 6.75 8.5H13.25C13.6642 8.5 14 8.16421 14 7.75C14 7.33579 13.6642 7 13.25 7H6.75Z" fill="#03D47C"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M3.8254 1.70739L4.75 2.49991L6.1746 1.27882C6.36185 1.11832 6.63815 1.11832 6.8254 1.27882L8.25 2.49991L9.6746 1.27882C9.86185 1.11832 10.1382 1.11832 10.3254 1.27882L11.75 2.49991L13.1746 1.27882C13.3618 1.11832 13.6382 1.11832 13.8254 1.27882L15.25 2.49991L16.1746 1.70739C16.4989 1.42939 17 1.65984 17 2.08702V7.1875L17.6306 7.69986C17.8643 7.88975 18 8.17484 18 8.47597V18C18 19.1046 17.1046 20 16 20H4C2.89543 20 2 19.1046 2 18V8.47597C2 8.17484 2.1357 7.88975 2.36941 7.69986L3 7.1875V2.08702C3 1.65984 3.50106 1.42939 3.8254 1.70739ZM5 4.91975V9.52781L10 12L15 9.52781V4.91975L13.5 3.63403L11.75 5.13403L10 3.63403L8.25 5.13403L6.5 3.63403L5 4.91975Z" fill="#03D47C"/> +</svg> diff --git a/src/CONST.ts b/src/CONST.ts index b07b622cec05..b7358716434a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -787,6 +787,7 @@ const CONST = { EXPENSE: 'expense', IOU: 'iou', TASK: 'task', + INVOICE: 'invoice', }, CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { @@ -1411,6 +1412,7 @@ const CONST = { SPLIT: 'split', REQUEST: 'request', TRACK_EXPENSE: 'track-expense', + INVOICE: 'invoice', }, REQUEST_TYPE: { DISTANCE: 'distance', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 877e4972a3ec..a823266802fb 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -86,6 +86,7 @@ import ImageCropSquareMask from '@assets/images/image-crop-square-mask.svg'; import Info from '@assets/images/info.svg'; import QBOSquare from '@assets/images/integrationicons/qbo-icon-square.svg'; import XeroSquare from '@assets/images/integrationicons/xero-icon-square.svg'; +import InvoiceGeneric from '@assets/images/invoice-generic.svg'; import Invoice from '@assets/images/invoice.svg'; import Key from '@assets/images/key.svg'; import Keyboard from '@assets/images/keyboard.svg'; @@ -247,6 +248,7 @@ export { ImageCropSquareMask, Info, Invoice, + InvoiceGeneric, Key, Keyboard, Link, diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index eec6cce0a1a3..0bc85269e764 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -25,6 +25,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; import {policyPropTypes} from '@pages/workspace/withPolicy'; @@ -39,6 +40,7 @@ import ConfirmedRoute from './ConfirmedRoute'; import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; import * as Expensicons from './Icon/Expensicons'; +import MenuItem from './MenuItem'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import optionPropTypes from './optionPropTypes'; import OptionsSelector from './OptionsSelector'; @@ -172,6 +174,9 @@ const propTypes = { /** Transaction that represents the money request */ transaction: transactionPropTypes, + + /** The list of all policies */ + allPolicies: PropTypes.objectOf(policyPropTypes), }; const defaultProps = { @@ -249,6 +254,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ session: {accountID}, shouldShowSmartScanFields, transaction, + allPolicies, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -258,6 +264,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE; const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit); @@ -280,6 +287,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + const senderWorkspace = useMemo(() => { + const senderWorkspaceParticipant = _.find(pickedParticipants, (pickedParticipant) => pickedParticipant.policyID); + return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant.policyID}`]; + }, [allPolicies, pickedParticipants]); + + const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0, [allPolicies]); + // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); @@ -400,7 +414,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; - if (isTypeTrackExpense) { + if (isTypeInvoice) { + text = translate('iou.sendInvoice', {amount: formattedAmount}); + } else if (isTypeTrackExpense) { text = translate('iou.trackExpense'); } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); @@ -684,6 +700,29 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ + { + item: ( + <MenuItem + key={translate('workspace.invoices.sendFrom')} + shouldShowRightIcon={!isReadOnly && canUpdateSenderWorkspace} + title={lodashGet(senderWorkspace, 'name')} + icon={lodashGet(senderWorkspace, 'avatar') || getDefaultWorkspaceAvatar(lodashGet(senderWorkspace, 'name'))} + iconType={CONST.ICON_TYPE_WORKSPACE} + description={translate('workspace.common.workspace')} + label={translate('workspace.invoices.sendFrom')} + isLabelHoverable={false} + interactive={!isReadOnly && canUpdateSenderWorkspace} + onPress={() => { + // Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm || !canUpdateSenderWorkspace} + /> + ), + shouldShow: isTypeInvoice, + isSupplementary: false, + }, { item: ( <MenuItemWithTopDescription @@ -1053,5 +1092,8 @@ export default compose( policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 219199c25bc3..4ceb453ee5ea 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -2,6 +2,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -45,7 +46,10 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs, canUseTrackExpense); - const additionalText = moneyRequestOptions.map((item) => translate(`reportActionsView.iouTypes.${item}`)).join(', '); + const additionalText = moneyRequestOptions + .filter((item): item is ValueOf<typeof CONST.IOU.TYPE, 'SEND' | 'SPLIT' | 'REQUEST' | 'TRACK_EXPENSE'> => item !== CONST.IOU.TYPE.INVOICE) + .map((item) => translate(`reportActionsView.iouTypes.${item}`)) + .join(', '); const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy); const reportName = ReportUtils.getReportName(report); diff --git a/src/languages/en.ts b/src/languages/en.ts index 3b670f7b6ebc..17c08774acd8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -653,6 +653,7 @@ export default { payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} elsewhere` : `Pay elsewhere`), nextStep: 'Next Steps', finished: 'Finished', + sendInvoice: ({amount}: RequestAmountParams) => `Send ${amount} invoice`, requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`, requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `requested ${formattedAmount}${comment ? ` for ${comment}` : ''}`, trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`, @@ -2127,6 +2128,7 @@ export default { unlockVBACopy: "You're all set to accept payments by ACH or credit card!", viewUnpaidInvoices: 'View unpaid invoices', sendInvoice: 'Send invoice', + sendFrom: 'Send from', }, travel: { unlockConciergeBookingTravel: 'Unlock Concierge travel booking', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5027174b2922..b1cee1928152 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -649,6 +649,7 @@ export default { payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} de otra forma` : `Pagar de otra forma`), nextStep: 'Pasos Siguientes', finished: 'Finalizado', + sendInvoice: ({amount}: RequestAmountParams) => `Enviar factura de ${amount}`, requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicité ${formattedAmount}${comment ? ` para ${comment}` : ''}`, trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `seguimiento ${formattedAmount}${comment ? ` para ${comment}` : ''}`, @@ -2155,6 +2156,7 @@ export default { unlockVBACopy: '¡Todo listo para recibir pagos por transferencia o con tarjeta!', viewUnpaidInvoices: 'Ver facturas emitidas pendientes', sendInvoice: 'Enviar factura', + sendFrom: 'Enviado desde', }, travel: { unlockConciergeBookingTravel: 'Desbloquea la reserva de viajes con Concierge', diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 415872750243..6145b7cab156 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -102,10 +102,10 @@ function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { } /** - * Checks if the iou type is one of request, send, or split. + * Checks if the iou type is one of request, send, invoice or split. */ function isValidMoneyRequestType(iouType: string): boolean { - const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.TRACK_EXPENSE]; + const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.TRACK_EXPENSE, CONST.IOU.TYPE.INVOICE]; return moneyRequestType.includes(iouType); } diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 665830ca7167..f79b2ca4c608 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -18,7 +18,7 @@ type MemberEmailsToAccountIDs = Record<string, number>; * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. */ -function getActivePolicies(policies: OnyxCollection<Policy>): Policy[] | undefined { +function getActivePolicies(policies: OnyxCollection<Policy>): Policy[] { return Object.values(policies ?? {}).filter<Policy>( (policy): policy is Policy => policy !== null && policy && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !!policy.name && !!policy.id, ); @@ -317,6 +317,18 @@ function getPolicyIDFromNavigationState() { return getPolicyIDFromState(navigationRef.getRootState() as State<RootStackParamList>); } +/** Return active policies where current user is an admin */ +function getActiveAdminWorkspaces(policies: OnyxCollection<Policy>): Policy[] { + const activePolicies = getActivePolicies(policies); + + return activePolicies.filter((policy) => isPolicyAdmin(policy)); +} + +/** Whether the user can send invoice */ +function canSendInvoice(policies: OnyxCollection<Policy>): boolean { + return getActiveAdminWorkspaces(policies).length > 0; +} + export { getActivePolicies, hasAccountingConnections, @@ -355,6 +367,8 @@ export { getTaxByID, hasPolicyCategoriesError, getPolicyIDFromNavigationState, + getActiveAdminWorkspaces, + canSendInvoice, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index d6ab203e85a4..51dfc82490e1 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -261,6 +261,14 @@ Onyx.connect({ }, }); +let activePolicyID: OnyxEntry<string>; +Onyx.connect({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (value) => { + activePolicyID = value; + }, +}); + /** * Stores in Onyx the policy ID of the last workspace that was accessed by the user */ @@ -286,6 +294,16 @@ function getPolicy(policyID: string | undefined): Policy | EmptyObject { return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? {}; } +/** + * Returns the primary policy for the user + */ +function getPrimaryPolicy(): Policy | undefined { + const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); + const primaryPolicy: Policy | null | undefined = allPolicies?.[activePolicyID ?? '']; + + return primaryPolicy ?? activeAdminWorkspaces[0]; +} + /** * Check if the user has any active free policies (aka workspaces) */ @@ -5075,6 +5093,7 @@ export { updatePolicyDistanceRateValue, setPolicyDistanceRatesEnabled, deletePolicyDistanceRates, + getPrimaryPolicy, }; export type {NewCustomUnit}; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 11a84c17f9ee..8ed1a62ebb9c 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -143,6 +143,11 @@ function AttachmentPickerWithMenuItems({ text: translate('iou.trackExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), }, + [CONST.IOU.TYPE.INVOICE]: { + icon: Expensicons.Invoice, + text: translate('workspace.invoices.sendInvoice'), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? ''), + }, }; return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 4ce9b508365b..bbb9f6edbef9 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -15,6 +15,7 @@ import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; import * as IOU from '@userActions/IOU'; @@ -28,7 +29,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {QuickActionName} from '@src/types/onyx/QuickAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -type PolicySelector = Pick<OnyxTypes.Policy, 'type' | 'role' | 'isPolicyExpenseChatEnabled' | 'pendingAction' | 'avatar' | 'name'>; +type PolicySelector = Pick<OnyxTypes.Policy, 'type' | 'role' | 'isPolicyExpenseChatEnabled' | 'pendingAction' | 'avatar' | 'name' | 'id'>; type FloatingActionButtonAndPopoverOnyxProps = { /** The list of policies the user has access to. */ @@ -63,6 +64,7 @@ const policySelector = (policy: OnyxEntry<OnyxTypes.Policy>): PolicySelector => (policy && { type: policy.type, role: policy.role, + id: policy.id, isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, pendingAction: policy.pendingAction, avatar: policy.avatar, @@ -136,6 +138,8 @@ function FloatingActionButtonAndPopover( const isFocused = useIsFocused(); const prevIsFocused = usePrevious(isFocused); + const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection<OnyxTypes.Policy>), [allPolicies]); + const quickActionReport: OnyxEntry<OnyxTypes.Report> = useMemo(() => (quickAction ? ReportUtils.getReport(quickAction.chatReportID) : null), [quickAction]); const quickActionPolicy = allPolicies ? allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`] : undefined; @@ -309,6 +313,23 @@ function FloatingActionButtonAndPopover( ), ), }, + ...(canSendInvoice + ? [ + { + icon: Expensicons.InvoiceGeneric, + text: translate('workspace.invoices.sendInvoice'), + onSelected: () => + interceptAnonymousUser(() => + IOU.startMoneyRequest( + CONST.IOU.TYPE.INVOICE, + // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + ReportUtils.generateReportID(), + ), + ), + }, + ] + : []), { icon: Expensicons.Task, text: translate('newTaskPage.assignTask'), diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index cb078fac133c..0a4556ef0a72 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -79,6 +79,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), [CONST.IOU.TYPE.TRACK_EXPENSE]: translate('iou.trackExpense'), + [CONST.IOU.TYPE.INVOICE]: translate('workspace.invoices.sendInvoice'), }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); @@ -110,10 +111,12 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = iouType !== CONST.IOU.TYPE.TRACK_EXPENSE && (canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate); + const shouldDisplayDistanceRequest = + ![CONST.IOU.TYPE.TRACK_EXPENSE, CONST.IOU.TYPE.INVOICE].includes(iouType) && (canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate); + const shouldDisplayScanRequest = iouType !== CONST.IOU.TYPE.INVOICE; // Allow the user to create the request if we are creating the request in global menu or the report can create the request - const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); + const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || true; // TODO: update ReportUtils.canCreateRequest to include invoice const navigateBack = () => { Navigation.dismissModal(); @@ -166,7 +169,7 @@ function IOURequestStartPage({ tabBar={TabSelector} > <TopTab.Screen name={CONST.TAB_REQUEST.MANUAL}>{() => <IOURequestStepAmount route={route} />}</TopTab.Screen> - <TopTab.Screen name={CONST.TAB_REQUEST.SCAN}>{() => <IOURequestStepScan route={route} />}</TopTab.Screen> + {shouldDisplayScanRequest && <TopTab.Screen name={CONST.TAB_REQUEST.SCAN}>{() => <IOURequestStepScan route={route} />}</TopTab.Screen>} {shouldDisplayDistanceRequest && <TopTab.Screen name={CONST.TAB_REQUEST.DISTANCE}>{() => <IOURequestStepDistance route={route} />}</TopTab.Screen>} </OnyxTabNavigator> ) : ( diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index a05167d5cedf..ae179a926fbe 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -20,6 +20,7 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as Policy from '@userActions/Policy'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -95,6 +96,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; + const shouldShowReferralBanner = !dismissedReferralBanners[referralContentType] && iouType !== CONST.IOU.TYPE.INVOICE; + useEffect(() => { Report.searchInServer(debouncedSearchTerm.trim()); }, [debouncedSearchTerm]); @@ -194,13 +197,26 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const addSingleParticipant = useCallback( (option) => { - onParticipantsAdded([ + const newParticipants = [ { ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), selected: true, iouType, }, - ]); + ]; + + if (iouType === CONST.IOU.TYPE.INVOICE) { + // TODO: update the logic if we create from existing invoice room + const primaryPolicy = Policy.getPrimaryPolicy(); + + newParticipants.push({ + policyID: primaryPolicy.id, + selected: false, + iouType, + }); + } + + onParticipantsAdded(newParticipants); onFinish(); }, // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to trigger this callback when iouType changes @@ -267,7 +283,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - const isAllowedToSplit = (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && iouType !== CONST.IOU.TYPE.SEND; + const isAllowedToSplit = (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && ![CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.INVOICE].includes(iouType); const handleConfirmSelection = useCallback( (keyEvent, option) => { @@ -289,7 +305,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const footerContent = useMemo( () => ( <View> - {!dismissedReferralBanners[referralContentType] && ( + {shouldShowReferralBanner && ( <View style={[styles.flexShrink0, !!participants.length && !shouldShowSplitBillErrorMessage && styles.pb5]}> <ReferralProgramCTA referralContentType={referralContentType} /> </View> diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index d20a576d279e..d33ea06e1801 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -89,6 +89,9 @@ function IOURequestStepConfirmation({ if (iouType === CONST.IOU.TYPE.SEND) { return translate('common.send'); } + if (iouType === CONST.IOU.TYPE.INVOICE) { + return translate('workspace.invoices.sendInvoice'); + } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); }, [iouType, transaction, translate]); @@ -96,6 +99,11 @@ function IOURequestStepConfirmation({ () => transaction?.participants?.map((participant) => { const participantAccountID = participant.accountID ?? 0; + + if (participant.policyID && iouType === CONST.IOU.TYPE.INVOICE) { + return participant; + } + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }), [transaction?.participants, personalDetails], diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 7ccbdb18ee03..b63545e57220 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -50,6 +50,9 @@ function IOURequestStepParticipants({ if (iouType === CONST.IOU.TYPE.SEND) { return translate('common.send'); } + if (iouType === CONST.IOU.TYPE.INVOICE) { + return translate('workspace.invoices.sendInvoice'); + } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); }, [iouType, transaction, translate, isSplitRequest]); @@ -124,6 +127,8 @@ function IOURequestStepParticipants({ nextStepIOUType = CONST.IOU.TYPE.SPLIT; } else if (iouType === CONST.IOU.TYPE.SEND) { nextStepIOUType = CONST.IOU.TYPE.SEND; + } else if (iouType === CONST.IOU.TYPE.INVOICE) { + nextStepIOUType = CONST.IOU.TYPE.INVOICE; } IOU.setMoneyRequestTag(transactionID, ''); From b6689d3f8bfae76167c13639b12bc012a81a4372 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Thu, 11 Apr 2024 10:42:04 +0200 Subject: [PATCH 068/580] Implement send from (select sender) screen --- src/ROUTES.ts | 5 + src/SCREENS.ts | 1 + ...oraryForRefactorRequestConfirmationList.js | 2 +- .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 6 + .../request/step/IOURequestStepSendFrom.tsx | 122 ++++++++++++++++++ .../step/withFullTransactionOrNotFound.tsx | 3 +- .../step/withWritableReportOrNotFound.tsx | 3 +- src/styles/index.ts | 10 ++ 10 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 src/pages/iou/request/step/IOURequestStepSendFrom.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 60fca9fac87b..5f077bda037f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -316,6 +316,11 @@ const ROUTES = { getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) => `${action}/${iouType}/start/${transactionID}/${reportID}` as const, }, + MONEY_REQUEST_STEP_SEND_FROM: { + route: 'create/:iouType/from/:transactionID/:reportID', + getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`create/${iouType}/from/${transactionID}/${reportID}`, backTo), + }, MONEY_REQUEST_STEP_CONFIRMATION: { route: ':action/:iouType/confirmation/:transactionID/:reportID', getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b3c2012e90d2..572df76e283f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -155,6 +155,7 @@ const SCREENS = { STEP_WAYPOINT: 'Money_Request_Step_Waypoint', STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount', STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', + STEP_SEND_FROM: 'Money_Request_Step_Send_From', PARTICIPANTS: 'Money_Request_Participants', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 0bc85269e764..1f4aaa15e1ed 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -713,7 +713,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isLabelHoverable={false} interactive={!isReadOnly && canUpdateSenderWorkspace} onPress={() => { - // Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); }} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index b4c97ed40556..51f425f5d9bd 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -86,6 +86,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator<MoneyRequestNa [SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../../pages/iou/request/step/IOURequestStepScan').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_TAG]: () => require('../../../../pages/iou/request/step/IOURequestStepTag').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: () => require('../../../../pages/iou/request/step/IOURequestStepWaypoint').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: () => require('../../../../pages/iou/request/step/IOURequestStepSendFrom').default as React.ComponentType, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, [SCREENS.MONEY_REQUEST.HOLD]: () => require('../../../../pages/iou/HoldReasonPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index f7cdc54335ab..3c260f12d7bb 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -551,6 +551,7 @@ const config: LinkingOptions<RootStackParamList>['config'] = { }, }, }, + [SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: ROUTES.MONEY_REQUEST_STEP_SEND_FROM.route, [SCREENS.MONEY_REQUEST.STEP_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_CATEGORY]: ROUTES.MONEY_REQUEST_STEP_CATEGORY.route, [SCREENS.MONEY_REQUEST.STEP_CONFIRMATION]: ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ed34b8ee3856..1a779f0c30bb 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -356,6 +356,12 @@ type MoneyRequestNavigatorParamList = { reportID: string; backTo: string; }; + [SCREENS.MONEY_REQUEST.STEP_SEND_FROM]: { + iouType: ValueOf<typeof CONST.IOU.TYPE>; + transactionID: string; + reportID: string; + backTo: Routes; + }; [SCREENS.MONEY_REQUEST.PARTICIPANTS]: { iouType: string; reportID: string; diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx new file mode 100644 index 000000000000..49e47d030bbc --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -0,0 +1,122 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Policy} from '@src/types/onyx'; +import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; + +type WorkspaceListItem = ListItem & { + value: string; +}; + +type IOURequestStepSendFromOnyxProps = { + /** The list of all policies */ + allPolicies: OnyxCollection<Policy>; +}; + +type IOURequestStepSendFromProps = IOURequestStepSendFromOnyxProps & + WithWritableReportOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM> & + WithFullTransactionOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM>; + +function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestStepSendFromProps) { + const {translate} = useLocalize(); + const theme = useTheme(); + const styles = useThemeStyles(); + const {transactionID, backTo} = route.params; + + const workspaceOptions: WorkspaceListItem[] = useMemo(() => { + const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); + return activeAdminWorkspaces.map((policy) => ({ + text: policy.name, + value: policy.id, + keyForList: policy.id, + icons: [ + { + source: policy?.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy.name), + fallbackIcon: Expensicons.FallbackWorkspaceAvatar, + name: policy.name, + type: CONST.ICON_TYPE_WORKSPACE, + }, + ], + isSelected: !!transaction?.participants?.find((participant) => participant.policyID === policy.id), + })); + }, [allPolicies, transaction]); + + const navigateBack = () => { + Navigation.goBack(backTo); + }; + + const selectWorkspace = (item: WorkspaceListItem) => { + const newParticipants = (transaction?.participants ?? []).filter((participant) => participant.selected); + + newParticipants.push({ + policyID: item.value, + selected: false, + }); + + IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, newParticipants); + navigateBack(); + }; + + const renderCheckbox = useCallback( + (item: ListItem) => ( + <View style={[styles.roundCheckmarkWrapper, styles.mh2]}> + {item.isSelected && ( + <Icon + src={Expensicons.Checkmark} + fill={theme.success} + /> + )} + </View> + ), + [workspaceOptions], + ); + + return ( + <StepScreenWrapper + headerTitle={translate('workspace.invoices.sendFrom')} + onBackButtonPress={navigateBack} + shouldShowWrapper + testID={IOURequestStepSendFrom.displayName} + includeSafeAreaPaddingBottom + > + <SelectionList + sections={[{data: workspaceOptions, title: translate('common.workspaces')}]} + onSelectRow={selectWorkspace} + ListItem={UserListItem} + rightHandSideComponent={renderCheckbox} + /> + </StepScreenWrapper> + ); +} + +IOURequestStepSendFrom.displayName = 'IOURequestStepSendFrom'; + +export default withWritableReportOrNotFound( + withFullTransactionOrNotFound( + withOnyx<IOURequestStepSendFromProps, IOURequestStepSendFromOnyxProps>({ + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + })(IOURequestStepSendFrom), + ), +); diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 9fee88b45d0c..77ddad8f15ff 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -23,7 +23,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY - | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE; + | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE + | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM; type Route<T extends MoneyRequestRouteName> = RouteProp<MoneyRequestNavigatorParamList, T>; diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 515f6f97f280..9e9ea193b5f2 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -23,7 +23,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE - | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT; + | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT + | typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM; type Route<T extends MoneyRequestRouteName> = RouteProp<MoneyRequestNavigatorParamList, T>; diff --git a/src/styles/index.ts b/src/styles/index.ts index f165974119ff..13c78d9518c9 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3003,6 +3003,16 @@ const styles = (theme: ThemeColors) => alignSelf: 'center', }, + roundCheckmarkWrapper: { + alignItems: 'center', + justifyContent: 'center', + height: 24, + width: 24, + borderRadius: 12, + borderColor: theme.borderLighter, + borderWidth: 1, + }, + codeWordWrapper: { ...codeStyles.codeWordWrapper, }, From fc2e134a9a511d24a9d3a6b0f2d79243615cbd56 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 11 Apr 2024 14:44:42 +0200 Subject: [PATCH 069/580] refactor: rename AddressForm to AddressFormFields --- src/pages/EnablePayments/AdditionalDetailsStep.tsx | 4 ++-- .../{AddressForm.tsx => AddressFormFields.tsx} | 6 +++--- .../BeneficialOwnerDetailsFormSubsteps/AddressUBO.tsx | 4 ++-- .../BusinessInfo/substeps/AddressBusiness.tsx | 4 ++-- .../ReimbursementAccount/PersonalInfo/substeps/Address.tsx | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) rename src/pages/ReimbursementAccount/{AddressForm.tsx => AddressFormFields.tsx} (94%) diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index 46e7191f222b..867bbadf9ea7 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -19,7 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ValidationUtils from '@libs/ValidationUtils'; -import AddressForm from '@pages/ReimbursementAccount/AddressForm'; +import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -184,7 +184,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO defaultValue={PersonalDetailsUtils.extractFirstAndLastNameFromAvailableDetails(currentUserPersonalDetails).lastName} shouldSaveDraft /> - <AddressForm + <AddressFormFields inputKeys={{ street: 'addressStreet', city: 'addressCity', diff --git a/src/pages/ReimbursementAccount/AddressForm.tsx b/src/pages/ReimbursementAccount/AddressFormFields.tsx similarity index 94% rename from src/pages/ReimbursementAccount/AddressForm.tsx rename to src/pages/ReimbursementAccount/AddressFormFields.tsx index 22829eed521f..48af00cd4925 100644 --- a/src/pages/ReimbursementAccount/AddressForm.tsx +++ b/src/pages/ReimbursementAccount/AddressFormFields.tsx @@ -36,7 +36,7 @@ type AddressFormProps = { shouldSaveDraft?: boolean; }; -function AddressForm({shouldSaveDraft = false, defaultValues, values, errors, inputKeys, onFieldChange, streetTranslationKey}: AddressFormProps) { +function AddressFormFields({shouldSaveDraft = false, defaultValues, values, errors, inputKeys, onFieldChange, streetTranslationKey}: AddressFormProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -103,6 +103,6 @@ function AddressForm({shouldSaveDraft = false, defaultValues, values, errors, in ); } -AddressForm.displayName = 'AddressForm'; +AddressFormFields.displayName = 'AddressFormFields'; -export default AddressForm; +export default AddressFormFields; diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/AddressUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/AddressUBO.tsx index c02e3e238c80..c49f26abf80c 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/AddressUBO.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/AddressUBO.tsx @@ -9,7 +9,7 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ValidationUtils from '@libs/ValidationUtils'; -import AddressForm from '@pages/ReimbursementAccount/AddressForm'; +import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReimbursementAccountForm} from '@src/types/form'; @@ -74,7 +74,7 @@ function AddressUBO({reimbursementAccountDraft, onNext, isEditing, beneficialOwn > <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('beneficialOwnerInfoStep.enterTheOwnersAddress')}</Text> <Text style={[styles.textSupporting]}>{translate('common.noPO')}</Text> - <AddressForm + <AddressFormFields inputKeys={inputKeys} shouldSaveDraft={!isEditing} defaultValues={defaultValues} diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/AddressBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/AddressBusiness.tsx index f569d220662e..cca9e5619d59 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/AddressBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/AddressBusiness.tsx @@ -9,7 +9,7 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ValidationUtils from '@libs/ValidationUtils'; -import AddressForm from '@pages/ReimbursementAccount/AddressForm'; +import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {ReimbursementAccount} from '@src/types/onyx'; @@ -78,7 +78,7 @@ function AddressBusiness({reimbursementAccount, onNext, isEditing}: AddressBusin > <Text style={[styles.textHeadlineLineHeightXXL]}>{translate('businessInfoStep.enterYourCompanysAddress')}</Text> <Text>{translate('common.noPO')}</Text> - <AddressForm + <AddressFormFields inputKeys={INPUT_KEYS} shouldSaveDraft={!isEditing} defaultValues={defaultValues} diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.tsx index 59815f511b77..a0ec8220ce3e 100644 --- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.tsx +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Address.tsx @@ -10,7 +10,7 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ValidationUtils from '@libs/ValidationUtils'; -import AddressForm from '@pages/ReimbursementAccount/AddressForm'; +import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -77,7 +77,7 @@ function Address({reimbursementAccount, onNext, isEditing}: AddressProps) { <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.enterYourAddress')}</Text> <Text style={[styles.textSupporting]}>{translate('common.noPO')}</Text> - <AddressForm + <AddressFormFields inputKeys={INPUT_KEYS} streetTranslationKey="common.streetAddress" defaultValues={defaultValues} From 03111d4c3e7a3ccfcdf59e49fefd4b0f947e035e Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 11 Apr 2024 14:50:11 +0200 Subject: [PATCH 070/580] refactor: remove unused file --- .../EnablePayments/userWalletPropTypes.js | 31 ------------------- 1 file changed, 31 deletions(-) delete mode 100644 src/pages/EnablePayments/userWalletPropTypes.js diff --git a/src/pages/EnablePayments/userWalletPropTypes.js b/src/pages/EnablePayments/userWalletPropTypes.js deleted file mode 100644 index 53332479d4ec..000000000000 --- a/src/pages/EnablePayments/userWalletPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -/** User's wallet information */ -export default PropTypes.shape({ - /** The user's available wallet balance */ - availableBalance: PropTypes.number, - - /** The user's current wallet balance */ - currentBalance: PropTypes.number, - - /** What step in the activation flow are we on? */ - currentStep: PropTypes.string, - - /** Error code returned by the server */ - errorCode: PropTypes.string, - - /** If we should show the FailedKYC view after the user submitted their info with a non fixable error */ - shouldShowFailedKYC: PropTypes.bool, - - /** Status of wallet - e.g. SILVER or GOLD */ - tierName: PropTypes.string, - - /** Whether the kyc is pending and is yet to be confirmed */ - isPendingOnfidoResult: PropTypes.bool, - - /** The wallet's programID, used to show the correct terms. */ - walletProgramID: PropTypes.string, - - /** Whether the user has failed Onfido completely */ - hasFailedOnfido: PropTypes.bool, -}); From e572f6dff82408ce0bfb0571bc8d8bba9f2c4642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Thu, 11 Apr 2024 15:04:58 +0200 Subject: [PATCH 071/580] Fix jumping composer issue --- ios/Podfile.lock | 4 +- src/components/Composer/index.tsx | 70 +++++++------------ src/components/Composer/types.ts | 6 -- .../ComposerWithSuggestions.tsx | 25 ------- 4 files changed, 26 insertions(+), 79 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 32a8bca75bcd..67216d60f3ba 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1921,8 +1921,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 9f26224fce1233ffdad9fa4e56863e3de2190dc0 - Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 + Yoga: 13c8ef87792450193e117976337b8527b49e8c03 PODFILE CHECKSUM: a431c146e1501391834a2f299a74093bac53b530 -COCOAPODS: 1.13.0 +COCOAPODS: 1.15.2 diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 69cc6b208652..f3a73288161d 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -60,12 +60,10 @@ function Composer( autoFocus = false, isFullComposerAvailable = false, shouldCalculateCaretPosition = false, - numberOfLines: numberOfLinesProp = 0, isDisabled = false, onClear = () => {}, onPasteFile = () => {}, onSelectionChange = () => {}, - onNumberOfLinesChange = () => {}, setIsFullComposerAvailable = () => {}, checkComposerVisibility = () => false, selection: selectionProp = { @@ -82,11 +80,9 @@ function Composer( const theme = useTheme(); const styles = useThemeStyles(); const markdownStyle = useMarkdownStyle(); - const StyleUtils = useStyleUtils(); const {windowWidth} = useWindowDimensions(); const textRef = useRef<HTMLElement & RNText>(null); const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null); - const [numberOfLines, setNumberOfLines] = useState(numberOfLinesProp); const [selection, setSelection] = useState< | { start: number; @@ -103,13 +99,13 @@ function Composer( const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState<number | undefined>(); + const StyleUtils = useStyleUtils(); useEffect(() => { if (!shouldClear) { return; } textInput.current?.clear(); - setNumberOfLines(1); onClear(); }, [shouldClear, onClear]); @@ -244,33 +240,21 @@ function Composer( if (!textInput.current) { return; } + + const textInputHeight = textInput.current.clientHeight; // we reset the height to 0 to get the correct scrollHeight textInput.current.style.height = '0'; const computedStyle = window.getComputedStyle(textInput.current); - const lineHeight = parseInt(computedStyle.lineHeight, 10) || 20; - const paddingTopAndBottom = parseInt(computedStyle.paddingBottom, 10) + parseInt(computedStyle.paddingTop, 10); setTextInputWidth(computedStyle.width); - - const computedNumberOfLines = ComposerUtils.getNumberOfLines(lineHeight, paddingTopAndBottom, textInput.current.scrollHeight, maxLines); - const generalNumberOfLines = computedNumberOfLines === 0 ? numberOfLinesProp : computedNumberOfLines; - - onNumberOfLinesChange(generalNumberOfLines); - updateIsFullComposerAvailable({isFullComposerAvailable, setIsFullComposerAvailable}, generalNumberOfLines); - setNumberOfLines(generalNumberOfLines); + // updateIsFullComposerAvailable({isFullComposerAvailable, setIsFullComposerAvailable}, textInputHeight >= 70 ? 3 : 1); textInput.current.style.height = 'auto'; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, maxLines, numberOfLinesProp, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable, windowWidth]); + }, [value, maxLines, isFullComposerAvailable, setIsFullComposerAvailable, windowWidth]); useEffect(() => { updateNumberOfLines(); }, [updateNumberOfLines]); - const currentNumberOfLines = useMemo( - () => (isComposerFullSize ? undefined : numberOfLines), - - [isComposerFullSize, numberOfLines], - ); - useEffect(() => { if (!textInput.current) { return; @@ -333,7 +317,13 @@ function Composer( opacity: 0, }} > - <Text style={[StyleSheet.flatten([style, styles.noSelect]), numberOfLines < maxLines ? styles.overflowHidden : {}, {maxWidth: textInputWidth as DimensionValue}]}> + <Text + style={[ + StyleSheet.flatten([style, styles.noSelect]), + (textInput.current?.clientHeight ?? 0) < 330 ? styles.overflowHidden : {}, + {maxWidth: textInputWidth as DimensionValue}, + ]} + > {`${valueBeforeCaret} `} <Text numberOfLines={1} @@ -349,23 +339,19 @@ function Composer( if (shouldContainScroll) { return isScrollBarVisible ? [styles.overflowScroll, styles.overscrollBehaviorContain] : styles.overflowHidden; } - return [ - // We are hiding the scrollbar to prevent it from reducing the text input width, - // so we can get the correct scroll height while calculating the number of lines. - numberOfLines < maxLines ? styles.overflowHidden : {}, - ]; - }, [shouldContainScroll, isScrollBarVisible, maxLines, numberOfLines, styles.overflowHidden, styles.overflowScroll, styles.overscrollBehaviorContain]); + return styles.overflowAuto; + }, [shouldContainScroll, styles.overflowAuto, styles.overflowScroll, styles.overscrollBehaviorContain, styles.overflowHidden, isScrollBarVisible]); const inputStyleMemo = useMemo( () => [ StyleSheet.flatten([style, {outline: 'none'}]), - StyleUtils.getComposeTextAreaPadding(numberOfLines, isComposerFullSize), Browser.isMobileSafari() || Browser.isSafari() ? styles.rtlTextRenderForSafari : {}, scrollStyleMemo, + StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), isComposerFullSize ? ({height: '100%', maxHeight: 'none' as DimensionValue} as TextStyle) : undefined, ], - [numberOfLines, scrollStyleMemo, styles.rtlTextRenderForSafari, style, StyleUtils, isComposerFullSize], + [style, styles.rtlTextRenderForSafari, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize], ); return ( @@ -376,7 +362,7 @@ function Composer( placeholderTextColor={theme.placeholderText} ref={(el) => (textInput.current = el)} selection={selection} - style={inputStyleMemo} + style={[inputStyleMemo]} markdownStyle={markdownStyle} value={value} defaultValue={defaultValue} @@ -384,24 +370,16 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} - numberOfLines={currentNumberOfLines} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { - if (isReportActionCompose) { - ReportActionComposeFocusManager.onComposerFocus(null); - } else { - // While a user edits a comment, if they open the LHN menu, we want to ensure that - // the focus returns to the message edit composer after they click on a menu item (e.g. mark as read). - // To achieve this, we re-assign the focus callback here. - ReportActionComposeFocusManager.onComposerFocus(() => { - if (!textInput.current) { - return; - } - - textInput.current.focus(); - }); - } + ReportActionComposeFocusManager.onComposerFocus(() => { + if (!textInput.current) { + return; + } + + textInput.current.focus(); + }); props.onFocus?.(e); }} diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 6bc44aba69cd..531bcd03f8bf 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -21,15 +21,9 @@ type ComposerProps = TextInputProps & { /** The value of the comment box */ value?: string; - /** Number of lines for the comment */ - numberOfLines?: number; - /** Callback method handle when the input is changed */ onChangeText?: (numberOfLines: string) => void; - /** Callback method to update number of lines for the comment */ - onNumberOfLinesChange?: (numberOfLines: number) => void; - /** Callback method to handle pasting a file */ onPasteFile?: (file: File) => void; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index f8147dfda81d..a985ea3c57b3 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -63,9 +63,6 @@ type AnimatedRef = ReturnType<typeof useAnimatedRef>; type NewlyAddedChars = {startIndex: number; endIndex: number; diff: string}; type ComposerWithSuggestionsOnyxProps = { - /** The number of lines the comment should take up */ - numberOfLines: OnyxEntry<number>; - /** The parent report actions for the report */ parentReportActions: OnyxEntry<OnyxTypes.ReportActions>; @@ -209,7 +206,6 @@ function ComposerWithSuggestions( modal, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, parentReportActions, - numberOfLines, // Props: Report reportID, @@ -451,19 +447,6 @@ function ComposerWithSuggestions( ], ); - /** - * Update the number of lines for a comment in Onyx - */ - const updateNumberOfLines = useCallback( - (newNumberOfLines: number) => { - if (newNumberOfLines === numberOfLines) { - return; - } - Report.saveReportCommentNumberOfLines(reportID, newNumberOfLines); - }, - [reportID, numberOfLines], - ); - const prepareCommentAndResetComposer = useCallback((): string => { const trimmedComment = commentRef.current.trim(); const commentLength = ReportUtils.getCommentLength(trimmedComment); @@ -752,8 +735,6 @@ function ComposerWithSuggestions( isComposerFullSize={isComposerFullSize} value={value} testID="composer" - numberOfLines={numberOfLines ?? undefined} - onNumberOfLinesChange={updateNumberOfLines} shouldCalculateCaretPosition onLayout={onLayout} onScroll={hideSuggestionMenu} @@ -798,12 +779,6 @@ ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); export default withOnyx<ComposerWithSuggestionsProps & RefAttributes<ComposerRef>, ComposerWithSuggestionsOnyxProps>({ - numberOfLines: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, - // We might not have number of lines in onyx yet, for which the composer would be rendered as null - // during the first render, which we want to avoid: - initWithStoredValues: false, - }, modal: { key: ONYXKEYS.MODAL, }, From 059f946f4b531c91189bc398ed4131a63f5292bb Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Thu, 11 Apr 2024 17:44:51 +0200 Subject: [PATCH 072/580] Implement send invoice functionality --- ...oraryForRefactorRequestConfirmationList.js | 2 +- src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/API/parameters/SendInvoiceParams.ts | 19 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/ReportUtils.ts | 24 + src/libs/actions/IOU.ts | 469 +++++++++++++++++- .../step/IOURequestStepConfirmation.tsx | 6 + 9 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 src/libs/API/parameters/SendInvoiceParams.ts diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 2486d4d1e08b..0d9ef8257174 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -291,7 +291,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant.policyID}`]; }, [allPolicies, pickedParticipants]); - const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0, [allPolicies]); + const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0 && transaction.isFromGlobalCreate, [allPolicies]); // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); diff --git a/src/languages/en.ts b/src/languages/en.ts index 9410f061df11..d6112b3458e5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -503,6 +503,7 @@ export default { beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` to chat about anything ${workspaceName} related.`, beginningOfChatHistoryUserRoomPartOne: 'Collaboration starts here! 🎉\nUse this space to chat about anything ', beginningOfChatHistoryUserRoomPartTwo: ' related.', + beginningOfChatHistoryInvoiceRoom: 'Collaboration starts here! 🎉 Use this room to view, discuss, and pay invoices.', beginningOfChatHistory: 'This is the beginning of your chat with ', beginningOfChatHistoryPolicyExpenseChatPartOne: 'Collaboration between ', beginningOfChatHistoryPolicyExpenseChatPartTwo: ' and ', @@ -699,6 +700,7 @@ export default { invalidSplit: 'Split amounts do not equal total amount', other: 'Unexpected error, please try again later', genericCreateFailureMessage: 'Unexpected error requesting money, please try again later', + genericCreateInvoiceFailureMessage: 'Unexpected error sending invoice, please try again later', receiptFailureMessage: "The receipt didn't upload. ", saveFileMessage: 'Download the file ', loseFileMessage: 'or dismiss this error and lose it', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9bf1ffcc593a..278ebb31c152 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -499,6 +499,7 @@ export default { beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, beginningOfChatHistoryUserRoomPartOne: '¡Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', beginningOfChatHistoryUserRoomPartTwo: '.', + beginningOfChatHistoryInvoiceRoom: '¡Este es el lugar para colaborar! 🎉 Utilice esta sala para ver, discutir y pagar facturas.', beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ', beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ', beginningOfChatHistoryPolicyExpenseChatPartTwo: ' y ', @@ -697,6 +698,7 @@ export default { invalidSplit: 'La suma de las partes no equivale al importe total', other: 'Error inesperado, por favor inténtalo más tarde', genericCreateFailureMessage: 'Error inesperado solicitando dinero. Por favor, inténtalo más tarde', + genericCreateInvoiceFailureMessage: 'Error inesperado al enviar la factura, inténtalo de nuevo más tarde', receiptFailureMessage: 'El recibo no se subió. ', saveFileMessage: 'Guarda el archivo ', loseFileMessage: 'o descarta este error y piérdelo', diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts new file mode 100644 index 000000000000..d727bf33f090 --- /dev/null +++ b/src/libs/API/parameters/SendInvoiceParams.ts @@ -0,0 +1,19 @@ +type SendInvoiceParams = { + senderWorkspaceID: string; + accountID: number; + receiverEmail?: string; // used when there is no existing room + receiverInvoiceRoomID?: string; // optional param used only when the user sends an invoice to an existing room + amount: number; + currency: string; + merchant: string; + date: string; + category?: string; + optimisticInvoiceRoomID?: string; + optimisticCreatedChatReportActionID: string; + optimisticInvoiceReportID: string; + optimisticReportPreviewReportActionID: string; + optimisticTransactionID: string; + optimisticTransactionThreadReportID: string; +}; + +export default SendInvoiceParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 6f5505b263fb..8737c34a6931 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -199,3 +199,4 @@ export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicy export type {default as SetPolicyCurrencyDefaultParams} from './SetPolicyCurrencyDefaultParams'; export type {default as UpdatePolicyConnectionConfigParams} from './UpdatePolicyConnectionConfigParams'; export type {default as RenamePolicyTaxParams} from './RenamePolicyTaxParams'; +export type {default as SendInvoiceParams} from './SendInvoiceParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index fc19ba60693c..9c8df237484a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -198,6 +198,7 @@ const WRITE_COMMANDS = { UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue', SET_POLICY_DISTANCE_RATES_ENABLED: 'SetPolicyDistanceRatesEnabled', DELETE_POLICY_DISTANCE_RATES: 'DeletePolicyDistanceRates', + SEND_INVOICE: 'SendInvoice', } as const; type WriteCommand = ValueOf<typeof WRITE_COMMANDS>; @@ -394,6 +395,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams; [WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams; + [WRITE_COMMANDS.SEND_INVOICE]: Parameters.SendInvoiceParams; }; const READ_COMMANDS = { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 85f5c414dbe4..5f11e6099076 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3268,6 +3268,29 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe return result.trim().length ? result : formula; } +/** Builds an optimistic Invoice report with a randomly generated reportID */ +function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, receiverAccountID: number, receiverName: string, total: number, currency: string): OptimisticExpenseReport { + const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency); + + return { + reportID: generateReportID(), + chatReportID, + policyID, + type: CONST.REPORT.TYPE.INVOICE, + ownerAccountID: currentUserAccountID, + managerID: receiverAccountID, + currency, + // We don’t translate reportName because the server response is always in English + reportName: `${receiverName} owes ${formattedTotal}`, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + total, + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + parentReportID: chatReportID, + lastVisibleActionCreated: DateUtils.getDBTime(), + }; +} + /** * Builds an optimistic Expense report with a randomly generated reportID * @@ -5979,6 +6002,7 @@ export { hasActionsWithErrors, getGroupChatName, getOutstandingChildRequest, + buildOptimisticInvoiceReport, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c2d462bbc4a8..05f9127944f3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -17,6 +17,7 @@ import type { PayMoneyRequestParams, ReplaceReceiptParams, RequestMoneyParams, + SendInvoiceParams, SendMoneyParams, SplitBillParams, StartSplitBillParams, @@ -41,7 +42,7 @@ import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; +import type {OptimisticAddCommentReportAction, OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as UserUtils from '@libs/UserUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; @@ -804,6 +805,307 @@ function buildOnyxDataForMoneyRequest( return [optimisticData, successData, failureData]; } +/** Builds the Onyx data for an invoice */ +function buildOnyxDataForInvoice( + chatReport: OnyxEntry<OnyxTypes.Report>, + iouReport: OnyxTypes.Report, + transaction: OnyxTypes.Transaction, + chatCreatedAction: OptimisticCreatedReportAction, + iouCreatedAction: OptimisticCreatedReportAction, + iouAction: OptimisticIOUReportAction, + reportPreviewAction: ReportAction, + optimisticPolicyRecentlyUsedCategories: string[], + optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags, + isNewChatReport: boolean, + transactionThreadReport: OptimisticChatReport, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction, + inviteReportAction?: OptimisticAddCommentReportAction, + chatBeginningReportAction?: OptimisticAddCommentReportAction, + // policy?: OnyxEntry<OnyxTypes.Policy>, + // policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, + // policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, +): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { + const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); + const optimisticData: OnyxUpdate[] = []; + + if (chatReport) { + optimisticData.push({ + // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page + onyxMethod: isNewChatReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + ...chatReport, + lastReadTime: DateUtils.getDBTime(), + lastMessageTranslationKey: '', + iouReportID: iouReport.reportID, + ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), + }, + }); + } + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + ...iouReport, + lastMessageText: iouAction.message?.[0]?.text, + lastMessageHtml: iouAction.message?.[0]?.html, + pendingFields: { + createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: transaction, + }, + isNewChatReport && inviteReportAction && chatBeginningReportAction + ? { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [inviteReportAction.reportActionID]: inviteReportAction as ReportAction, + [chatBeginningReportAction.reportActionID]: chatBeginningReportAction as ReportAction, + [chatCreatedAction.reportActionID]: chatCreatedAction, + [reportPreviewAction.reportActionID]: reportPreviewAction, + }, + } + : { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + value: { + [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: transactionThreadReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, + value: { + action: CONST.QUICK_ACTIONS.REQUEST_MANUAL, + chatReportID: chatReport?.reportID, + isFirstQuickAction: isEmptyObject(quickAction), + }, + }, + // Remove the temporary transaction used during the creation flow + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, + value: null, + }, + ); + + if (optimisticPolicyRecentlyUsedCategories.length) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${iouReport.policyID}`, + value: optimisticPolicyRecentlyUsedCategories, + }); + } + + if (!isEmptyObject(optimisticPolicyRecentlyUsedTags)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`, + value: optimisticPolicyRecentlyUsedTags, + }); + } + + const successData: OnyxUpdate[] = []; + + if (isNewChatReport) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + pendingFields: null, + errorFields: null, + isOptimisticReport: false, + }, + }); + } + + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + pendingFields: null, + errorFields: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + pendingFields: null, + errorFields: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + pendingAction: null, + pendingFields: clearedPendingFields, + }, + }, + + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + ...(isNewChatReport + ? { + [chatCreatedAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + } + : {}), + [reportPreviewAction.reportActionID]: { + pendingAction: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + value: { + [iouCreatedAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + [iouAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + ); + + const errorKey = DateUtils.getMicroseconds(); + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + iouReportID: chatReport?.iouReportID, + lastReadTime: chatReport?.lastReadTime, + pendingFields: null, + hasOutstandingChildRequest: chatReport?.hasOutstandingChildRequest, + ...(isNewChatReport + ? { + errorFields: { + createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + }, + } + : {}), + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + pendingFields: null, + errorFields: { + createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + errorFields: { + createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateInvoiceFailureMessage'), + pendingAction: null, + pendingFields: clearedPendingFields, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, + value: { + [iouCreatedAction.reportActionID]: { + // Disabling this line since transaction.filename can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, false, errorKey), + }, + [iouAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateInvoiceFailureMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateInvoiceFailureMessage', false, errorKey), + }, + }, + }, + ]; + + // We don't need to compute violations unless we're on a paid policy + // if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) { + // return [optimisticData, successData, failureData]; + // } + // + // const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); + // + // if (violationsOnyxData) { + // optimisticData.push(violationsOnyxData); + // failureData.push({ + // onyxMethod: Onyx.METHOD.SET, + // key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + // value: [], + // }); + // } + + return [optimisticData, successData, failureData]; +} + /** Builds the Onyx data for track expense */ function buildOnyxDataForTrackExpense( chatReport: OnyxEntry<OnyxTypes.Report>, @@ -1104,6 +1406,118 @@ function buildOnyxDataForTrackExpense( return [optimisticData, successData, failureData]; } +/** Gathers all the data needed to create an invoice. */ +function getSendInvoiceInformation(transaction: OnyxEntry<OnyxTypes.Transaction>, invoiceChatReport?: OnyxEntry<OnyxTypes.Report>, receipt?: Receipt) { + const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', billable, comment, participants} = transaction ?? {}; + const trimmedComment = (comment?.comment ?? '').trim(); + const senderWorkspaceID = participants?.find((participant) => participant?.policyID)?.policyID ?? ''; + const receiverAccountID = participants?.find((participant) => participant?.accountID)?.accountID ?? -1; + const receiver = ReportUtils.getPersonalDetailsForAccountID(receiverAccountID); + + // STEP 1: Get existing chat report OR build a new optimistic one + let isNewChatReport = false; + let chatReport = !isEmptyObject(invoiceChatReport) && invoiceChatReport?.reportID ? invoiceChatReport : null; + + if (!chatReport) { + isNewChatReport = true; + chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID]); // TODO: add additional logic for invoice room + } + + // STEP 3: Create a new optimistic invoice report. + const optimisticInvoiceReport = ReportUtils.buildOptimisticInvoiceReport(chatReport.reportID, senderWorkspaceID, receiverAccountID, receiver.displayName ?? '', amount, currency); + + // STEP 2: Build optimistic receipt and transaction + const receiptObject: Receipt = {}; + let filename; + if (receipt?.source) { + receiptObject.source = receipt.source; + receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY; + filename = receipt.name; + } + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction( + amount, + currency, + optimisticInvoiceReport.reportID, + trimmedComment, + created, + '', + '', + merchant, + receiptObject, + filename, + undefined, + category, + tag, + billable, + ); + + const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(optimisticInvoiceReport.policyID, category); + const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(optimisticInvoiceReport.policyID, tag); + + // STEP 4: Build optimistic reportActions. + let inviteReportAction; + let chatBeginningReportAction; + const [optimisticCreatedActionForChat, optimisticCreatedActionForIOUReport, iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = + ReportUtils.buildOptimisticMoneyRequestEntities( + optimisticInvoiceReport, + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount, + currency, + trimmedComment, + receiver.login ?? '', + [receiver], + optimisticTransaction.transactionID, + undefined, + false, + false, + receiptObject, + false, + ); + const reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, optimisticInvoiceReport, trimmedComment, optimisticTransaction); + + if (isNewChatReport) { + inviteReportAction = ReportUtils.buildOptimisticAddCommentReportAction(`${Localize.translateLocal('workspace.invite.invited')} ${receiver?.displayName}`).reportAction; + chatBeginningReportAction = { + ...ReportUtils.buildOptimisticAddCommentReportAction(Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom')).reportAction, + // whisperedToAccountIDs: [userAccountID], //TODO: fix + }; + } + + // STEP 4: Build Onyx Data + const [optimisticData, successData, failureData] = buildOnyxDataForInvoice( + chatReport, + optimisticInvoiceReport, + optimisticTransaction, + optimisticCreatedActionForChat, + optimisticCreatedActionForIOUReport, + iouAction, + reportPreviewAction, + optimisticPolicyRecentlyUsedCategories, + optimisticPolicyRecentlyUsedTags, + isNewChatReport, + optimisticTransactionThread, + optimisticCreatedActionForTransactionThread, + inviteReportAction, + chatBeginningReportAction, + ); + + return { + senderWorkspaceID, + receiver, + optimisticInvoiceRoomID: chatReport.reportID, + optimisticCreatedChatReportActionID: optimisticCreatedActionForChat.reportActionID, + optimisticInvoiceReportID: optimisticInvoiceReport.reportID, + optimisticReportPreviewReportActionID: reportPreviewAction.reportActionID, + optimisticTransactionID: optimisticTransaction.transactionID, + optimisticTransactionThreadReportID: optimisticTransactionThread.reportID, + onyxData: { + optimisticData, + successData, + failureData, + }, + }; +} + /** * Gathers all the data needed to make a money request. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then * it creates optimistic versions of them and uses those instead @@ -2352,6 +2766,58 @@ function requestMoney( } } +function sendInvoice(currentUserAccountID: number, transaction: OnyxEntry<OnyxTypes.Transaction>, invoiceChatReport?: OnyxEntry<OnyxTypes.Report>, receiptFile?: Receipt) { + const { + senderWorkspaceID, + receiver, + optimisticInvoiceRoomID, + optimisticCreatedChatReportActionID, + optimisticInvoiceReportID, + optimisticReportPreviewReportActionID, + optimisticTransactionID, + optimisticTransactionThreadReportID, + onyxData, + } = getSendInvoiceInformation(transaction, invoiceChatReport, receiptFile); + + let parameters: SendInvoiceParams = { + senderWorkspaceID, + accountID: currentUserAccountID, + amount: transaction?.amount ?? 0, + currency: transaction?.currency ?? '', + merchant: transaction?.merchant ?? '', + category: transaction?.category, + date: transaction?.created ?? '', + optimisticInvoiceRoomID, + optimisticCreatedChatReportActionID, + optimisticInvoiceReportID, + optimisticReportPreviewReportActionID, + optimisticTransactionID, + optimisticTransactionThreadReportID, + }; + + if (!invoiceChatReport) { + parameters = { + ...parameters, + receiverEmail: receiver.login, + }; + } + + if (!transaction?.isFromGlobalCreate && invoiceChatReport) { + parameters = { + ...parameters, + receiverInvoiceRoomID: invoiceChatReport.reportID, + }; + } + + API.write(WRITE_COMMANDS.SEND_INVOICE, parameters, onyxData); + + resetMoneyRequestInfo(); + Navigation.dismissModal(invoiceChatReport?.reportID); + if (invoiceChatReport?.reportID) { + Report.notifyNewAction(invoiceChatReport.reportID, receiver.accountID); + } +} + /** * Track an expense */ @@ -5537,4 +6003,5 @@ export { trackExpense, canIOUBePaid, canApproveIOU, + sendInvoice, }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index dc4c8847db88..9a91e903feba 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -341,6 +341,12 @@ function IOURequestStepConfirmation({ return; } + if (iouType === CONST.IOU.TYPE.INVOICE) { + // TODO: check if we need to navigate to the report + IOU.sendInvoice(currentUserPersonalDetails.accountID, transaction, report, receiptFile); + return; + } + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { if (receiptFile && transaction) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. From d871f7ed766b9452c8575a62818d78fb2698bc45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Thu, 11 Apr 2024 17:52:00 +0200 Subject: [PATCH 073/580] Clear Composer component --- src/components/Composer/index.native.tsx | 2 -- src/components/Composer/index.tsx | 15 ++++++-------- .../updateNumberOfLines/index.native.ts | 20 ------------------- .../updateNumberOfLines/index.ts | 17 +++++++++++++++- 4 files changed, 22 insertions(+), 32 deletions(-) delete mode 100644 src/libs/ComposerUtils/updateNumberOfLines/index.native.ts diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 6691c068eb3a..1c4f4344b27e 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -21,7 +21,6 @@ function Composer( isComposerFullSize = false, setIsFullComposerAvailable = () => {}, autoFocus = false, - isFullComposerAvailable = false, style, // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. @@ -80,7 +79,6 @@ function Composer( style={[composerStyle, maxHeightStyle]} markdownStyle={markdownStyle} autoFocus={autoFocus} - isFullComposerAvailable={isFullComposerAvailable} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index f3a73288161d..fcc7ca41386e 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -17,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; -import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; @@ -126,7 +125,6 @@ function Composer( return; } const webEvent = event as BaseSyntheticEvent<TextInputSelectionChangeEventData>; - if (shouldCalculateCaretPosition) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { @@ -236,24 +234,22 @@ function Composer( * Check the current scrollHeight of the textarea (minus any padding) and * divide by line height to get the total number of rows for the textarea. */ - const updateNumberOfLines = useCallback(() => { + const updateTextInputWidth = useCallback(() => { if (!textInput.current) { return; } - - const textInputHeight = textInput.current.clientHeight; + const textInputHeight = textInput.current.style.height; // we reset the height to 0 to get the correct scrollHeight textInput.current.style.height = '0'; const computedStyle = window.getComputedStyle(textInput.current); setTextInputWidth(computedStyle.width); - // updateIsFullComposerAvailable({isFullComposerAvailable, setIsFullComposerAvailable}, textInputHeight >= 70 ? 3 : 1); - textInput.current.style.height = 'auto'; + textInput.current.style.height = textInputHeight; // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, maxLines, isFullComposerAvailable, setIsFullComposerAvailable, windowWidth]); useEffect(() => { - updateNumberOfLines(); - }, [updateNumberOfLines]); + updateTextInputWidth(); + }, [updateTextInputWidth]); useEffect(() => { if (!textInput.current) { @@ -370,6 +366,7 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} + onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles)} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts deleted file mode 100644 index 5a7676d8bfbd..000000000000 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts +++ /dev/null @@ -1,20 +0,0 @@ -import getNumberOfLines from '@libs/ComposerUtils/getNumberOfLines'; -import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; -import type UpdateNumberOfLines from './types'; - -/** - * Check the current scrollHeight of the textarea (minus any padding) and - * divide by line height to get the total number of rows for the textarea. - */ -const updateNumberOfLines: UpdateNumberOfLines = (props, event, styles) => { - const lineHeight = styles.textInputCompose.lineHeight ?? 0; - const paddingTopAndBottom = styles.textInputComposeSpacing.paddingVertical * 2; - const inputHeight = event?.nativeEvent?.contentSize?.height ?? null; - if (!inputHeight) { - return; - } - const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight); - updateIsFullComposerAvailable(props, numberOfLines); -}; - -export default updateNumberOfLines; diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.ts index 3037fb99c8b1..5a7676d8bfbd 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/index.ts @@ -1,5 +1,20 @@ +import getNumberOfLines from '@libs/ComposerUtils/getNumberOfLines'; +import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import type UpdateNumberOfLines from './types'; -const updateNumberOfLines: UpdateNumberOfLines = () => {}; +/** + * Check the current scrollHeight of the textarea (minus any padding) and + * divide by line height to get the total number of rows for the textarea. + */ +const updateNumberOfLines: UpdateNumberOfLines = (props, event, styles) => { + const lineHeight = styles.textInputCompose.lineHeight ?? 0; + const paddingTopAndBottom = styles.textInputComposeSpacing.paddingVertical * 2; + const inputHeight = event?.nativeEvent?.contentSize?.height ?? null; + if (!inputHeight) { + return; + } + const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight); + updateIsFullComposerAvailable(props, numberOfLines); +}; export default updateNumberOfLines; From 10038d38a40b074f1e0d54c7c920485d7989faf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Thu, 11 Apr 2024 17:53:16 +0200 Subject: [PATCH 074/580] Change updateTextInputWidth function comment --- src/components/Composer/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index fcc7ca41386e..3817c386c729 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -231,8 +231,7 @@ function Composer( ); /** - * Check the current scrollHeight of the textarea (minus any padding) and - * divide by line height to get the total number of rows for the textarea. + * Check the current text input width and update the state with the new width. */ const updateTextInputWidth = useCallback(() => { if (!textInput.current) { From 56e3b9a52d0565af69111bbc25d42a58acd0c277 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 12 Apr 2024 08:52:10 +0200 Subject: [PATCH 075/580] Fix ts issues after merging main --- ...raryForRefactorRequestConfirmationList.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 93e6b27b9434..48f0735ee3ec 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -5,7 +5,7 @@ import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} fr import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry, OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; @@ -68,6 +68,9 @@ type MoneyRequestConfirmationListOnyxProps = { /** Unit and rate used for if the money request is a distance request */ mileageRate: OnyxEntry<DefaultMileageRate>; + + /** The list of all policies */ + allPolicies: OnyxCollection<OnyxTypes.Policy>; }; type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { @@ -162,9 +165,6 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & hasSmartScanFailed?: boolean; reportActionID?: string; - - /** The list of all policies */ - allPolicies: OnyxCollection<OnyxTypes.Policy>, }; const getTaxAmount = (transaction: OnyxEntry<OnyxTypes.Transaction>, defaultTaxValue: string) => { @@ -248,11 +248,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); const senderWorkspace = useMemo(() => { - const senderWorkspaceParticipant = _.find(pickedParticipants, (pickedParticipant) => pickedParticipant.policyID); - return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant.policyID}`]; + const senderWorkspaceParticipant = pickedParticipants.find((pickedParticipant) => pickedParticipant.policyID); + return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant?.policyID}`]; }, [allPolicies, pickedParticipants]); - const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0 && transaction.isFromGlobalCreate, [allPolicies]); + const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0 && !!transaction?.isFromGlobalCreate, [allPolicies]); // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); @@ -661,15 +661,15 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ <MenuItem key={translate('workspace.invoices.sendFrom')} shouldShowRightIcon={!isReadOnly && canUpdateSenderWorkspace} - title={lodashGet(senderWorkspace, 'name')} - icon={lodashGet(senderWorkspace, 'avatar') || getDefaultWorkspaceAvatar(lodashGet(senderWorkspace, 'name'))} + title={senderWorkspace?.name} + icon={senderWorkspace?.avatar ?? getDefaultWorkspaceAvatar(senderWorkspace?.name)} iconType={CONST.ICON_TYPE_WORKSPACE} description={translate('workspace.common.workspace')} label={translate('workspace.invoices.sendFrom')} isLabelHoverable={false} interactive={!isReadOnly && canUpdateSenderWorkspace} onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); }} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} From 26810a949d069d63f6b1d98525e9259bb5db6b70 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 12 Apr 2024 15:25:42 +0700 Subject: [PATCH 076/580] Update report option when personal detail is changed --- src/components/OptionListContextProvider.tsx | 43 +++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index a83eeda5a419..067dee830c80 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -6,7 +6,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import type {OptionList} from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; +import type {PersonalDetails, Report} from '@src/types/onyx'; import {usePersonalDetails} from './OnyxProvider'; type OptionsListContextProps = { @@ -37,6 +37,13 @@ const OptionsListContext = createContext<OptionsListContextProps>({ areOptionsInitialized: false, }); +const isEqualPersonalDetail = (prevPersonalDetail: PersonalDetails | null, personalDetail: PersonalDetails | null) => ( + prevPersonalDetail?.firstName === personalDetail?.firstName && + prevPersonalDetail?.lastName === personalDetail?.lastName && + prevPersonalDetail?.login === personalDetail?.login && + prevPersonalDetail?.displayName === personalDetail?.displayName + ); + function OptionsListContextProvider({reports, children}: OptionsListProviderProps) { const areOptionsInitialized = useRef(false); const [options, setOptions] = useState<OptionList>({ @@ -45,6 +52,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp }); const personalDetails = usePersonalDetails(); + const prevPersonalDetails = useRef(personalDetails); const prevReports = usePrevious(reports); /** @@ -96,7 +104,6 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp newOptions.reports.push(reportOption); return newOptions; }); - // eslint-disable-next-line react-hooks/exhaustive-deps }, [reports]); /** @@ -108,14 +115,46 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp return; } + const newReportOptions: Array<{ + replaceIndex: number; + newReportOption: OptionsListUtils.SearchOption<Report>; + }> = []; + + Object.keys(personalDetails).forEach((accoutID) => { + const prevPersonalDetail = prevPersonalDetails.current[accoutID]; + const personalDetail = personalDetails[accoutID]; + + if (isEqualPersonalDetail(prevPersonalDetail, personalDetail)) { + return; + } + + Object.values(reports ?? {}) + .filter((report) => report?.participantAccountIDs?.includes(Number(accoutID))) + .forEach((report) => { + if (!report) { + return; + } + const newReportOption = OptionsListUtils.createOptionFromReport(report, personalDetails); + const replaceIndex = options.reports.findIndex((option) => option.reportID === report.reportID); + newReportOptions.push({ + newReportOption, + replaceIndex, + }); + }); + }); + // since personal details are not a collection, we need to recreate the whole list from scratch const newPersonalDetailsOptions = OptionsListUtils.createOptionList(personalDetails).personalDetails; setOptions((prevOptions) => { const newOptions = {...prevOptions}; newOptions.personalDetails = newPersonalDetailsOptions; + newReportOptions.forEach((newReportOption) => (newOptions.reports[newReportOption.replaceIndex] = newReportOption.newReportOption)); return newOptions; }); + + prevPersonalDetails.current = personalDetails; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [personalDetails]); const loadOptions = useCallback(() => { From 23be9e4fa72aead7f01f7702876c895d03abffbd Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 12 Apr 2024 10:41:28 +0200 Subject: [PATCH 077/580] Code improvements --- src/CONST.ts | 1 + ...raryForRefactorRequestConfirmationList.tsx | 2 +- src/components/ReportWelcomeText.tsx | 2 +- src/libs/API/parameters/SendInvoiceParams.ts | 4 ++-- src/libs/PolicyUtils.ts | 1 - src/libs/ReportUtils.ts | 8 +++++++ src/libs/actions/IOU.ts | 21 +++++++++++++++---- src/libs/actions/Policy.ts | 10 +-------- src/pages/iou/request/IOURequestStartPage.js | 12 ++++++++++- ...yForRefactorRequestParticipantsSelector.js | 15 +++++++++---- .../step/IOURequestStepConfirmation.tsx | 16 ++------------ .../request/step/IOURequestStepSendFrom.tsx | 2 +- 12 files changed, 56 insertions(+), 38 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 7bc9401f5560..76a6dbbd8227 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -52,6 +52,7 @@ const chatTypes = { POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', SELF_DM: 'selfDM', + INVOICE: 'invoice', } as const; // Explicit type annotation is required diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 48f0735ee3ec..2ed699530915 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -662,7 +662,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ key={translate('workspace.invoices.sendFrom')} shouldShowRightIcon={!isReadOnly && canUpdateSenderWorkspace} title={senderWorkspace?.name} - icon={senderWorkspace?.avatar ?? getDefaultWorkspaceAvatar(senderWorkspace?.name)} + icon={senderWorkspace?.avatar ? senderWorkspace?.avatar : getDefaultWorkspaceAvatar(senderWorkspace?.name)} iconType={CONST.ICON_TYPE_WORKSPACE} description={translate('workspace.common.workspace')} label={translate('workspace.invoices.sendFrom')} diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 4ceb453ee5ea..11265fcfedf9 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -47,7 +47,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs, canUseTrackExpense); const additionalText = moneyRequestOptions - .filter((item): item is ValueOf<typeof CONST.IOU.TYPE, 'SEND' | 'SPLIT' | 'REQUEST' | 'TRACK_EXPENSE'> => item !== CONST.IOU.TYPE.INVOICE) + .filter((item): item is ValueOf<Omit<typeof CONST.IOU.TYPE, 'INVOICE'>> => item !== CONST.IOU.TYPE.INVOICE) .map((item) => translate(`reportActionsView.iouTypes.${item}`)) .join(', '); const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy); diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts index d727bf33f090..7b074657233e 100644 --- a/src/libs/API/parameters/SendInvoiceParams.ts +++ b/src/libs/API/parameters/SendInvoiceParams.ts @@ -1,8 +1,8 @@ type SendInvoiceParams = { senderWorkspaceID: string; accountID: number; - receiverEmail?: string; // used when there is no existing room - receiverInvoiceRoomID?: string; // optional param used only when the user sends an invoice to an existing room + receiverEmail?: string; + receiverInvoiceRoomID?: string; amount: number; currency: string; merchant: string; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 6a6189c9e4ff..976ae614de44 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -316,7 +316,6 @@ function getPolicyIDFromNavigationState() { /** Return active policies where current user is an admin */ function getActiveAdminWorkspaces(policies: OnyxCollection<Policy>): Policy[] { const activePolicies = getActivePolicies(policies); - return activePolicies.filter((policy) => isPolicyAdmin(policy)); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ddf7f5e5cc8e..c9706f7fe1e6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -860,6 +860,13 @@ function isPolicyExpenseChat(report: OnyxEntry<Report> | Participant | EmptyObje return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || (report?.isPolicyExpenseChat ?? false); } +/** + * Whether the provided report is an Invoice room chat. + */ +function isInvoiceRoom(report: OnyxEntry<Report>): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; +} + /** * Whether the provided report belongs to a Control policy and is an expense chat */ @@ -6048,6 +6055,7 @@ export { getGroupChatName, getOutstandingChildRequest, buildOptimisticInvoiceReport, + isInvoiceRoom, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5259a81933e6..ca49c8a4001b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5746,10 +5746,23 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report?.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); - const participants: Participant[] = - ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport - ? [{accountID: 0, reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] - : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + let participants: Participant[] = []; + + if (ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport) { + participants = [{accountID: 0, reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}]; + } else { + participants = (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + + if (ReportUtils.isInvoiceRoom(chatReport)) { + participants = [ + ...participants, + { + policyID: chatReport?.policyID, + selected: false, + }, + ]; + } + } Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 51dfc82490e1..9dd0cf1034ab 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -261,14 +261,6 @@ Onyx.connect({ }, }); -let activePolicyID: OnyxEntry<string>; -Onyx.connect({ - key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, - callback: (value) => { - activePolicyID = value; - }, -}); - /** * Stores in Onyx the policy ID of the last workspace that was accessed by the user */ @@ -297,7 +289,7 @@ function getPolicy(policyID: string | undefined): Policy | EmptyObject { /** * Returns the primary policy for the user */ -function getPrimaryPolicy(): Policy | undefined { +function getPrimaryPolicy(activePolicyID?: string): Policy | undefined { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); const primaryPolicy: Policy | null | undefined = allPolicies?.[activePolicyID ?? '']; diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 9cb334dbcf5a..8744f6163bd7 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -21,9 +21,11 @@ import * as IOUUtils from '@libs/IOUUtils'; import * as KeyDownPressListener from '@libs/KeyboardShortcut/KeyDownPressListener'; import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import reportPropTypes from '@pages/reportPropTypes'; +import {policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -51,6 +53,9 @@ const propTypes = { /** The transaction being modified */ transaction: transactionPropTypes, + + /** The list of all policies */ + allPolicies: PropTypes.objectOf(policyPropTypes.policy), }; const defaultProps = { @@ -58,6 +63,7 @@ const defaultProps = { policy: {}, selectedTab: CONST.TAB_REQUEST.SCAN, transaction: {}, + allPolicies: {}, }; function IOURequestStartPage({ @@ -69,6 +75,7 @@ function IOURequestStartPage({ }, selectedTab, transaction, + allPolicies, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -117,7 +124,7 @@ function IOURequestStartPage({ const shouldDisplayScanRequest = iouType !== CONST.IOU.TYPE.INVOICE; // Allow the user to create the request if we are creating the request in global menu or the report can create the request - const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || true; // TODO: update ReportUtils.canCreateRequest to include invoice + const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || PolicyUtils.canSendInvoice(allPolicies); const navigateBack = () => { Navigation.dismissModal(); @@ -201,4 +208,7 @@ export default withOnyx({ transaction: { key: ({route}) => `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${lodashGet(route, 'params.transactionID', '0')}`, }, + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, })(IOURequestStartPage); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2e0720bc41a0..c3359e3822bc 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -52,14 +52,18 @@ const propTypes = { /** The request type, ie. manual, scan, distance */ iouRequestType: PropTypes.oneOf(_.values(CONST.IOU.REQUEST_TYPE)).isRequired, + + /** ID of the user's active policy */ + activePolicyID: PropTypes.string, }; const defaultProps = { participants: [], betas: [], + activePolicyID: '', }; -function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participants, onFinish, onParticipantsAdded, iouType, iouRequestType}) { +function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participants, onFinish, onParticipantsAdded, iouType, iouRequestType, activePolicyID}) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); @@ -188,8 +192,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan ]; if (iouType === CONST.IOU.TYPE.INVOICE) { - // TODO: update the logic if we create from existing invoice room - const primaryPolicy = Policy.getPrimaryPolicy(); + const primaryPolicy = Policy.getPrimaryPolicy(activePolicyID); newParticipants.push({ policyID: primaryPolicy.id, @@ -384,6 +387,9 @@ export default withOnyx({ betas: { key: ONYXKEYS.BETAS, }, + activePolicyID: { + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + }, })( memo( MoneyTemporaryForRefactorRequestParticipantsSelector, @@ -391,6 +397,7 @@ export default withOnyx({ _.isEqual(prevProps.participants, nextProps.participants) && prevProps.iouRequestType === nextProps.iouRequestType && prevProps.iouType === nextProps.iouType && - _.isEqual(prevProps.betas, nextProps.betas), + _.isEqual(prevProps.betas, nextProps.betas) && + prevProps.activePolicyID === nextProps.activePolicyID, ), ); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 1a7fd0772d5d..8390a2e5f6a2 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -106,7 +106,7 @@ function IOURequestStepConfirmation({ return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); }) ?? [], - [transaction?.participants, personalDetails], + [transaction?.participants, personalDetails, iouType], ); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); const formHasBeenSubmitted = useRef(false); @@ -342,7 +342,6 @@ function IOURequestStepConfirmation({ } if (iouType === CONST.IOU.TYPE.INVOICE) { - // TODO: check if we need to navigate to the report IOU.sendInvoice(currentUserPersonalDetails.accountID, transaction, report, receiptFile); return; } @@ -420,18 +419,7 @@ function IOURequestStepConfirmation({ requestMoney(selectedParticipants, trimmedComment); }, - [ - transaction, - iouType, - receiptFile, - requestType, - requestMoney, - currentUserPersonalDetails.login, - currentUserPersonalDetails.accountID, - report?.reportID, - trackExpense, - createDistanceRequest, - ], + [transaction, report, iouType, receiptFile, requestType, requestMoney, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, trackExpense, createDistanceRequest], ); /** diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index 49e47d030bbc..069bd9edd671 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -66,7 +66,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte }; const selectWorkspace = (item: WorkspaceListItem) => { - const newParticipants = (transaction?.participants ?? []).filter((participant) => participant.selected); + const newParticipants = (transaction?.participants ?? []).filter((participant) => participant.accountID); newParticipants.push({ policyID: item.value, From f889f818b980d65fa78680330dfc6debb39b8463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Fri, 12 Apr 2024 10:51:20 +0200 Subject: [PATCH 078/580] Add review changes --- ios/Podfile.lock | 4 ++-- src/components/Composer/index.tsx | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 67216d60f3ba..32a8bca75bcd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1921,8 +1921,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 9f26224fce1233ffdad9fa4e56863e3de2190dc0 - Yoga: 13c8ef87792450193e117976337b8527b49e8c03 + Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 PODFILE CHECKSUM: a431c146e1501391834a2f299a74093bac53b530 -COCOAPODS: 1.15.2 +COCOAPODS: 1.13.0 diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3817c386c729..ae8ab69cef0d 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -237,12 +237,8 @@ function Composer( if (!textInput.current) { return; } - const textInputHeight = textInput.current.style.height; - // we reset the height to 0 to get the correct scrollHeight - textInput.current.style.height = '0'; const computedStyle = window.getComputedStyle(textInput.current); setTextInputWidth(computedStyle.width); - textInput.current.style.height = textInputHeight; // eslint-disable-next-line react-hooks/exhaustive-deps }, [value, maxLines, isFullComposerAvailable, setIsFullComposerAvailable, windowWidth]); From fd404290c183b54e7fbe45349181f1a09c3dbe32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Fri, 12 Apr 2024 11:04:55 +0200 Subject: [PATCH 079/580] Bump react-native-live-markdown version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d58118b7d268..9b71088ac2e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.38", + "@expensify/react-native-live-markdown": "0.1.53", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -3097,9 +3097,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.38", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.38.tgz", - "integrity": "sha512-0EcXvK/eqeJdesX8DBibJ+V2KX9n5Gbmg0fWTk93mGOUA70h3W6lO68nuch40X+RgQdOgIf50BMfzbBzzVdJwA==", + "version": "0.1.53", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.53.tgz", + "integrity": "sha512-E4nn3v2zz+0C9eLJfqOeJFebDHovvrNiSEgSbe3thFVgV25lTfPEJiuIXP1IT/zwzp0UadhDNhUGOV3m0ISNkA==", "engines": { "node": ">= 18.0.0" }, diff --git a/package.json b/package.json index 5c03af2be2f7..0e6cae7e93b0 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.38", + "@expensify/react-native-live-markdown": "0.1.53", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", From 7f41cf81464cd4d25f5cf3b684ad3b13a4da267b Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 12 Apr 2024 16:39:50 +0700 Subject: [PATCH 080/580] remove unnecessary change --- src/components/OptionListContextProvider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 067dee830c80..dff85fd8753b 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -104,6 +104,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp newOptions.reports.push(reportOption); return newOptions; }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [reports]); /** From 8cef4549a071cce524078add24e3bf74a1588eac Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 12 Apr 2024 17:08:03 +0700 Subject: [PATCH 081/580] fix selfDM case --- src/components/OptionListContextProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index dff85fd8753b..e63ce2c63faa 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -130,7 +130,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp } Object.values(reports ?? {}) - .filter((report) => report?.participantAccountIDs?.includes(Number(accoutID))) + .filter((report) => (report?.participantAccountIDs?.includes(Number(accoutID)) || (ReportUtils.isSelfDM(report) && report?.ownerAccountID === Number(accoutID)))) .forEach((report) => { if (!report) { return; From 36b645eca269823c952bbde284f323034bd85665 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 12 Apr 2024 12:10:35 +0200 Subject: [PATCH 082/580] IOU file updates and lint fixes --- ...raryForRefactorRequestConfirmationList.tsx | 7 +- src/libs/ReportUtils.ts | 14 +++- src/libs/actions/IOU.ts | 74 +++++++++++++------ ...yForRefactorRequestParticipantsSelector.js | 2 +- .../step/IOURequestStepConfirmation.tsx | 8 +- .../request/step/IOURequestStepSendFrom.tsx | 2 +- src/types/onyx/Report.ts | 13 ++++ 7 files changed, 87 insertions(+), 33 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 2ed699530915..5f9cea934508 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -252,7 +252,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant?.policyID}`]; }, [allPolicies, pickedParticipants]); - const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0 && !!transaction?.isFromGlobalCreate, [allPolicies]); + const canUpdateSenderWorkspace = useMemo( + () => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0 && !!transaction?.isFromGlobalCreate, + [allPolicies, transaction?.isFromGlobalCreate], + ); // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); @@ -392,7 +395,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ value: iouType, }, ]; - }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); + }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount, isTypeInvoice]); const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c9706f7fe1e6..542cd41c451a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -140,6 +140,7 @@ type OptimisticAddCommentReportAction = Pick< | 'childStatusNum' | 'childStateNum' | 'errors' + | 'whisperedToAccountIDs' > & {isOptimisticAction: boolean}; type OptimisticReportAction = { @@ -281,6 +282,7 @@ type OptimisticChatReport = Pick< | 'visibility' | 'description' | 'writeCapability' + | 'invoiceReceiver' > & { isOptimisticReport: true; }; @@ -3842,7 +3844,7 @@ function buildOptimisticChatReport( }, {} as Participants); const currentTime = DateUtils.getDBTime(); const isNewlyCreatedWorkspaceChat = chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT && isOwnPolicyExpenseChat; - return { + const optimisticChatReport: OptimisticChatReport = { isOptimisticReport: true, type: CONST.REPORT.TYPE.CHAT, chatType, @@ -3873,6 +3875,16 @@ function buildOptimisticChatReport( description, writeCapability, }; + + if (chatType === CONST.REPORT.CHAT_TYPE.INVOICE) { + // TODO: update to support workspace receiver when workspace-workspace invoice room exists + optimisticChatReport.invoiceReceiver = { + type: 'individual', + accountID: participantList[0], + }; + } + + return optimisticChatReport; } /** diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ca49c8a4001b..3dfb014e25af 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -821,9 +821,9 @@ function buildOnyxDataForInvoice( transactionThreadCreatedReportAction: OptimisticCreatedReportAction, inviteReportAction?: OptimisticAddCommentReportAction, chatBeginningReportAction?: OptimisticAddCommentReportAction, - // policy?: OnyxEntry<OnyxTypes.Policy>, - // policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, - // policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, + policy?: OnyxEntry<OnyxTypes.Policy>, + policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, + policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); const optimisticData: OnyxUpdate[] = []; @@ -1088,20 +1088,20 @@ function buildOnyxDataForInvoice( ]; // We don't need to compute violations unless we're on a paid policy - // if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) { - // return [optimisticData, successData, failureData]; - // } - // - // const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); - // - // if (violationsOnyxData) { - // optimisticData.push(violationsOnyxData); - // failureData.push({ - // onyxMethod: Onyx.METHOD.SET, - // key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, - // value: [], - // }); - // } + if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) { + return [optimisticData, successData, failureData]; + } + + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); + + if (violationsOnyxData) { + optimisticData.push(violationsOnyxData); + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: [], + }); + } return [optimisticData, successData, failureData]; } @@ -1407,7 +1407,14 @@ function buildOnyxDataForTrackExpense( } /** Gathers all the data needed to create an invoice. */ -function getSendInvoiceInformation(transaction: OnyxEntry<OnyxTypes.Transaction>, invoiceChatReport?: OnyxEntry<OnyxTypes.Report>, receipt?: Receipt) { +function getSendInvoiceInformation( + transaction: OnyxEntry<OnyxTypes.Transaction>, + invoiceChatReport?: OnyxEntry<OnyxTypes.Report>, + receipt?: Receipt, + policy?: OnyxEntry<OnyxTypes.Policy>, + policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, + policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, +) { const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.policyID)?.policyID ?? ''; @@ -1420,7 +1427,7 @@ function getSendInvoiceInformation(transaction: OnyxEntry<OnyxTypes.Transaction> if (!chatReport) { isNewChatReport = true; - chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID]); // TODO: add additional logic for invoice room + chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE); } // STEP 3: Create a new optimistic invoice report. @@ -1455,8 +1462,8 @@ function getSendInvoiceInformation(transaction: OnyxEntry<OnyxTypes.Transaction> const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(optimisticInvoiceReport.policyID, tag); // STEP 4: Build optimistic reportActions. - let inviteReportAction; - let chatBeginningReportAction; + let inviteReportAction: OptimisticAddCommentReportAction | undefined; + let chatBeginningReportAction: OptimisticAddCommentReportAction | undefined; const [optimisticCreatedActionForChat, optimisticCreatedActionForIOUReport, iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( optimisticInvoiceReport, @@ -1479,7 +1486,7 @@ function getSendInvoiceInformation(transaction: OnyxEntry<OnyxTypes.Transaction> inviteReportAction = ReportUtils.buildOptimisticAddCommentReportAction(`${Localize.translateLocal('workspace.invite.invited')} ${receiver?.displayName}`).reportAction; chatBeginningReportAction = { ...ReportUtils.buildOptimisticAddCommentReportAction(Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom')).reportAction, - // whisperedToAccountIDs: [userAccountID], //TODO: fix + whisperedToAccountIDs: [userAccountID], }; } @@ -1499,6 +1506,9 @@ function getSendInvoiceInformation(transaction: OnyxEntry<OnyxTypes.Transaction> optimisticCreatedActionForTransactionThread, inviteReportAction, chatBeginningReportAction, + policy, + policyTagList, + policyCategories, ); return { @@ -2766,7 +2776,15 @@ function requestMoney( } } -function sendInvoice(currentUserAccountID: number, transaction: OnyxEntry<OnyxTypes.Transaction>, invoiceChatReport?: OnyxEntry<OnyxTypes.Report>, receiptFile?: Receipt) { +function sendInvoice( + currentUserAccountID: number, + transaction: OnyxEntry<OnyxTypes.Transaction>, + invoiceChatReport?: OnyxEntry<OnyxTypes.Report>, + receiptFile?: Receipt, + policy?: OnyxEntry<OnyxTypes.Policy>, + policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, + policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, +) { const { senderWorkspaceID, receiver, @@ -2777,7 +2795,7 @@ function sendInvoice(currentUserAccountID: number, transaction: OnyxEntry<OnyxTy optimisticTransactionID, optimisticTransactionThreadReportID, onyxData, - } = getSendInvoiceInformation(transaction, invoiceChatReport, receiptFile); + } = getSendInvoiceInformation(transaction, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); let parameters: SendInvoiceParams = { senderWorkspaceID, @@ -5951,6 +5969,13 @@ function savePreferredPaymentMethod(policyID: string, paymentMethod: PaymentMeth Onyx.merge(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {[policyID]: paymentMethod}); } +/** Get report policy id of IOU request */ +function getIOURequestPolicyID(transaction: OnyxEntry<OnyxTypes.Transaction>, report: OnyxEntry<OnyxTypes.Report>): string { + // Workspace sender will exist for invoices + const workspaceSender = transaction?.participants?.find((participant) => participant.policyID); + return workspaceSender?.policyID ?? report?.policyID ?? '0'; +} + export type {GPSPoint as GpsPoint, IOURequestType}; export { setMoneyRequestParticipants, @@ -6015,4 +6040,5 @@ export { canIOUBePaid, canApproveIOU, sendInvoice, + getIOURequestPolicyID, }; diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index c3359e3822bc..dffd42e52688 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -324,7 +324,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participan )} </> ); - }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate]); + }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate, shouldShowReferralBanner]); const itemRightSideComponent = useCallback( (item) => { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 8390a2e5f6a2..be3fea36e7de 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -342,7 +342,7 @@ function IOURequestStepConfirmation({ } if (iouType === CONST.IOU.TYPE.INVOICE) { - IOU.sendInvoice(currentUserPersonalDetails.accountID, transaction, report, receiptFile); + IOU.sendInvoice(currentUserPersonalDetails.accountID, transaction, report, receiptFile, policy, policyTags, policyCategories); return; } @@ -532,13 +532,13 @@ IOURequestStepConfirmation.displayName = 'IOURequestStepConfirmation'; const IOURequestStepConfirmationWithOnyx = withOnyx<IOURequestStepConfirmationProps, IOURequestStepConfirmationOnyxProps>({ policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY}${IOU.getIOURequestPolicyID(transaction, report)}`, }, policyCategories: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${IOU.getIOURequestPolicyID(transaction, report)}`, }, policyTags: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, + key: ({report, transaction}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${IOU.getIOURequestPolicyID(transaction, report)}`, }, })(IOURequestStepConfirmation); /* eslint-disable rulesdir/no-negated-variables */ diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index 069bd9edd671..a90da053982b 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -59,7 +59,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte ], isSelected: !!transaction?.participants?.find((participant) => participant.policyID === policy.id), })); - }, [allPolicies, transaction]); + }, [allPolicies, transaction, styles.mh2, styles.roundCheckmarkWrapper, theme.success]); const navigateBack = () => { Navigation.goBack(backTo); diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 7e39e7f65a22..1c7fece532a8 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -28,6 +28,16 @@ type Participant = { role?: 'admin' | 'member'; }; +type InvoiceReceiver = + | { + type: 'individual'; + accountID: number; + } + | { + type: 'policy'; + policyID: string; + }; + type Participants = Record<number, Participant>; type Report = OnyxCommon.OnyxValueWithOfflineFeedback< @@ -128,6 +138,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Report cached total */ cachedTotal?: string; + /** Invoice room receiver data */ + invoiceReceiver?: InvoiceReceiver; + lastMessageTranslationKey?: string; parentReportID?: string; parentReportActionID?: string; From f611c60b0ddf339dbff767d9563d87e2841e6043 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 12 Apr 2024 15:12:24 +0200 Subject: [PATCH 083/580] Implement buildOptimisticInviteReportAction function --- src/libs/ReportUtils.ts | 41 +++++++++++++++++++++++++++++++ src/libs/actions/IOU.ts | 23 ++++++----------- src/types/onyx/OriginalMessage.ts | 1 + 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 542cd41c451a..8a27967e955e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -43,6 +43,7 @@ import type { OriginalMessageCreated, OriginalMessageReimbursementDequeued, OriginalMessageRenamed, + OriginalMessageRoomChangeLog, PaymentMethodType, ReimbursementDeQueuedMessage, } from '@src/types/onyx/OriginalMessage'; @@ -116,6 +117,8 @@ type SpendBreakdown = { type ParticipantDetails = [number, string, UserUtils.AvatarSource, UserUtils.AvatarSource]; +type OptimisticInviteReportAction = ReportActionBase & OriginalMessageRoomChangeLog; + type OptimisticAddCommentReportAction = Pick< ReportAction, | 'reportActionID' @@ -3104,6 +3107,42 @@ function getPolicyDescriptionText(policy: OnyxEntry<Policy>): string { return parser.htmlToText(policy.description); } +function buildOptimisticInviteReportAction(invitedUserDisplayName: string, invitedUserID: number): OptimisticInviteReportAction { + const text = `${Localize.translateLocal('workspace.invite.invited')} ${invitedUserDisplayName}`; + const commentText = getParsedComment(text); + const parser = new ExpensiMark(); + + return { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM, + actorAccountID: currentUserAccountID, + person: [ + { + style: 'strong', + text: allPersonalDetails?.[currentUserAccountID ?? -1]?.displayName ?? currentUserEmail, + type: 'TEXT', + }, + ], + automatic: false, + avatar: allPersonalDetails?.[currentUserAccountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + created: DateUtils.getDBTime(), + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + html: commentText, + text: parser.htmlToText(commentText), + }, + ], + originalMessage: { + targetAccountIDs: [invitedUserID], + }, + isFirstItem: false, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + shouldShow: true, + isOptimisticAction: true, + }; +} + function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); @@ -6067,6 +6106,7 @@ export { getGroupChatName, getOutstandingChildRequest, buildOptimisticInvoiceReport, + buildOptimisticInviteReportAction, isInvoiceRoom, }; @@ -6082,4 +6122,5 @@ export type { Ancestor, OptimisticIOUReportAction, TransactionDetails, + OptimisticInviteReportAction, }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 3dfb014e25af..03cb96da1f74 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -42,7 +42,7 @@ import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import type {OptimisticAddCommentReportAction, OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; +import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticInviteReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as UserUtils from '@libs/UserUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; @@ -819,8 +819,7 @@ function buildOnyxDataForInvoice( isNewChatReport: boolean, transactionThreadReport: OptimisticChatReport, transactionThreadCreatedReportAction: OptimisticCreatedReportAction, - inviteReportAction?: OptimisticAddCommentReportAction, - chatBeginningReportAction?: OptimisticAddCommentReportAction, + inviteReportAction?: OptimisticInviteReportAction, policy?: OnyxEntry<OnyxTypes.Policy>, policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, @@ -861,13 +860,12 @@ function buildOnyxDataForInvoice( key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, value: transaction, }, - isNewChatReport && inviteReportAction && chatBeginningReportAction + isNewChatReport && inviteReportAction ? { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, value: { [inviteReportAction.reportActionID]: inviteReportAction as ReportAction, - [chatBeginningReportAction.reportActionID]: chatBeginningReportAction as ReportAction, [chatCreatedAction.reportActionID]: chatCreatedAction, [reportPreviewAction.reportActionID]: reportPreviewAction, }, @@ -1427,7 +1425,7 @@ function getSendInvoiceInformation( if (!chatReport) { isNewChatReport = true; - chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE); + chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE, senderWorkspaceID); } // STEP 3: Create a new optimistic invoice report. @@ -1462,8 +1460,7 @@ function getSendInvoiceInformation( const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(optimisticInvoiceReport.policyID, tag); // STEP 4: Build optimistic reportActions. - let inviteReportAction: OptimisticAddCommentReportAction | undefined; - let chatBeginningReportAction: OptimisticAddCommentReportAction | undefined; + let inviteReportAction: OptimisticInviteReportAction | undefined; const [optimisticCreatedActionForChat, optimisticCreatedActionForIOUReport, iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( optimisticInvoiceReport, @@ -1480,15 +1477,10 @@ function getSendInvoiceInformation( receiptObject, false, ); - const reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, optimisticInvoiceReport, trimmedComment, optimisticTransaction); - if (isNewChatReport) { - inviteReportAction = ReportUtils.buildOptimisticAddCommentReportAction(`${Localize.translateLocal('workspace.invite.invited')} ${receiver?.displayName}`).reportAction; - chatBeginningReportAction = { - ...ReportUtils.buildOptimisticAddCommentReportAction(Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom')).reportAction, - whisperedToAccountIDs: [userAccountID], - }; + inviteReportAction = ReportUtils.buildOptimisticInviteReportAction(receiver?.displayName ?? '', receiver.accountID ?? -1); } + const reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, optimisticInvoiceReport, trimmedComment, optimisticTransaction); // STEP 4: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForInvoice( @@ -1505,7 +1497,6 @@ function getSendInvoiceInformation( optimisticTransactionThread, optimisticCreatedActionForTransactionThread, inviteReportAction, - chatBeginningReportAction, policy, policyTagList, policyCategories, diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 2e24fe00539a..c48d6ed6417f 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -343,6 +343,7 @@ export type { OriginalMessageJoinPolicyChangeLog, OriginalMessageActionableMentionWhisper, OriginalMessageChronosOOOList, + OriginalMessageRoomChangeLog, OriginalMessageSource, OriginalMessageReimbursementDequeued, DecisionName, From c4b0fdbe908b2598e96815e4146730e15ef7bd3d Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 12 Apr 2024 15:19:57 +0200 Subject: [PATCH 084/580] Fix navigation --- src/libs/actions/IOU.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 03cb96da1f74..036949227726 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2821,10 +2821,8 @@ function sendInvoice( API.write(WRITE_COMMANDS.SEND_INVOICE, parameters, onyxData); resetMoneyRequestInfo(); - Navigation.dismissModal(invoiceChatReport?.reportID); - if (invoiceChatReport?.reportID) { - Report.notifyNewAction(invoiceChatReport.reportID, receiver.accountID); - } + Navigation.dismissModal(optimisticInvoiceRoomID); + Report.notifyNewAction(optimisticInvoiceRoomID, receiver.accountID); } /** From ad6829b0900f254312a8d097daa8d19e0a5f04eb Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 12 Apr 2024 17:20:18 +0200 Subject: [PATCH 085/580] Fix invoice creation from global in invoice chat room already exists --- src/libs/ReportUtils.ts | 21 +++++++++++++++++++++ src/libs/actions/IOU.ts | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8a27967e955e..4bd0e566939b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4676,6 +4676,26 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec ); } +/** + * Attempts to find an invoice chat report in onyx with the provided policyID and receiverID. + */ +function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection<Report> = allReports): OnyxEntry<Report> { + return ( + Object.values(reports ?? {}).find((report) => { + if (!report || !isInvoiceRoom(report)) { + return false; + } + + const isSameReceiver = + report.invoiceReceiver && + (('accountID' in report.invoiceReceiver && report.invoiceReceiver.accountID === receiverID) || + ('policyID' in report.invoiceReceiver && report.invoiceReceiver.policyID === receiverID)); + + return report.policyID === policyID && isSameReceiver; + }) ?? null + ); +} + /** * Attempts to find a report in onyx with the provided list of participants in given policy */ @@ -6108,6 +6128,7 @@ export { buildOptimisticInvoiceReport, buildOptimisticInviteReportAction, isInvoiceRoom, + getInvoiceChatByParticipants, }; export type { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 036949227726..e02de590d7e3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1423,6 +1423,10 @@ function getSendInvoiceInformation( let isNewChatReport = false; let chatReport = !isEmptyObject(invoiceChatReport) && invoiceChatReport?.reportID ? invoiceChatReport : null; + if (!chatReport) { + chatReport = ReportUtils.getInvoiceChatByParticipants(senderWorkspaceID, receiverAccountID); + } + if (!chatReport) { isNewChatReport = true; chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE, senderWorkspaceID); From bc9ce69816beb2b8389a11f9f597af3c8bc85e31 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Fri, 12 Apr 2024 20:36:01 +0200 Subject: [PATCH 086/580] fix PR comments --- src/ROUTES.ts | 16 +++++----- src/libs/Navigation/types.ts | 16 +++++----- .../Wallet/ActivatePhysicalCardPage.tsx | 17 ++++++----- .../settings/Wallet/ExpensifyCardPage.tsx | 30 +++++++++---------- .../settings/Wallet/ReportCardLostPage.tsx | 8 ++--- .../Wallet/ReportVirtualCardFraudPage.tsx | 10 +++---- 6 files changed, 50 insertions(+), 47 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9f91be460f29..36fc4aeb76a9 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -84,12 +84,12 @@ const ROUTES = { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { - route: 'settings/wallet/card/:domain/:cardId', - getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}` as const, + route: 'settings/wallet/card/:domain/:cardID', + getRoute: (domain: string, cardID: string) => `settings/wallet/card/${domain}/${cardID}` as const, }, SETTINGS_REPORT_FRAUD: { - route: 'settings/wallet/card/:domain/:cardId/report-virtual-fraud', - getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}/report-virtual-fraud` as const, + route: 'settings/wallet/card/:domain/:cardID/report-virtual-fraud', + getRoute: (domain: string, cardID: string) => `settings/wallet/card/${domain}/${cardID}/report-virtual-fraud` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: { route: 'settings/wallet/card/:domain/get-physical/name', @@ -118,12 +118,12 @@ const ROUTES = { SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { - route: 'settings/wallet/card/:domain/:cardId/report-card-lost-or-damaged', - getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}/report-card-lost-or-damaged` as const, + route: 'settings/wallet/card/:domain/:cardID/report-card-lost-or-damaged', + getRoute: (domain: string, cardID: string) => `settings/wallet/card/${domain}/${cardID}/report-card-lost-or-damaged` as const, }, SETTINGS_WALLET_CARD_ACTIVATE: { - route: 'settings/wallet/card/:domain/:cardId/activate', - getRoute: (domain: string, cardId: string) => `settings/wallet/card/${domain}/${cardId}/activate` as const, + route: 'settings/wallet/card/:domain/:cardID/activate', + getRoute: (domain: string, cardID: string) => `settings/wallet/card/${domain}/${cardID}/activate` as const, }, SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 12ceebc0b555..e0647be03980 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -114,20 +114,20 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: { /** domain of selected card */ domain: string; - /** cardId of selected card */ - cardId: string; + /** cardID of selected card */ + cardID: string; }; [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: { /** domain of selected card */ domain: string; - /** cardId of selected card */ - cardId: string; + /** cardID of selected card */ + cardID: string; }; [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: { /** domain of selected card */ domain: string; - /** cardId of selected card */ - cardId: string; + /** cardID of selected card */ + cardID: string; }; [SCREENS.SETTINGS.WALLET.CARD_GET_PHYSICAL.NAME]: { /** domain of selected card */ @@ -268,8 +268,8 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: { /** domain of selected card */ domain: string; - /** cardId of selected card */ - cardId: string; + /** cardID of selected card */ + cardID: string; }; [SCREENS.KEYBOARD_SHORTCUTS]: undefined; [SCREENS.SETTINGS.EXIT_SURVEY.REASON]: undefined; diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index 2dada28a6df0..86b44494b5fb 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -42,7 +42,7 @@ const MAGIC_INPUT_MIN_HEIGHT = 86; function ActivatePhysicalCardPage({ cardList, route: { - params: {domain = '', cardId = ''}, + params: {domain = '', cardID = ''}, }, }: ActivatePhysicalCardPageProps) { const theme = useTheme(); @@ -69,12 +69,15 @@ function ActivatePhysicalCardPage({ return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId)); - }, [cardId, cardList, domain, physicalCard?.isLoading, physicalCard?.state]); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID)); + }, [cardID, cardList, domain, physicalCard?.isLoading, physicalCard?.state]); useEffect( () => () => { - CardSettings.clearCardListErrors(physicalCard?.cardID ?? 0); + if (!physicalCard?.cardID) { + return; + } + CardSettings.clearCardListErrors(physicalCard?.cardID); }, [physicalCard?.cardID], ); @@ -93,8 +96,8 @@ function ActivatePhysicalCardPage({ const onCodeInput = (text: string) => { setFormError(''); - if (cardError) { - CardSettings.clearCardListErrors(physicalCard?.cardID ?? 0); + if (cardError && physicalCard?.cardID) { + CardSettings.clearCardListErrors(physicalCard?.cardID); } setLastFourDigits(text); @@ -118,7 +121,7 @@ function ActivatePhysicalCardPage({ return ( <IllustratedHeaderPageLayout title={translate('activateCardPage.activateCard')} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID))} backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.PREFERENCES.ROOT].backgroundColor} illustration={LottieAnimations.Magician} scrollViewContainerStyles={[styles.mnh100]} diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index f862ea1e4dd7..95e2631b0f95 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -78,22 +78,22 @@ function ExpensifyCardPage({ privatePersonalDetails, loginList, route: { - params: {domain = '', cardId = ''}, + params: {domain = '', cardID = ''}, }, }: ExpensifyCardPageProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const shouldDisplayCardDomain = !cardList?.[cardId].nameValuePairs?.issuedBy; - const pageTitle = shouldDisplayCardDomain ? translate('cardPage.expensifyCard') : cardList?.[cardId].nameValuePairs?.cardTitle ?? translate('cardPage.expensifyCard'); + const shouldDisplayCardDomain = !cardList?.[cardID].nameValuePairs?.issuedBy; + const pageTitle = shouldDisplayCardDomain ? translate('cardPage.expensifyCard') : cardList?.[cardID].nameValuePairs?.cardTitle ?? translate('cardPage.expensifyCard'); const [isNotFound, setIsNotFound] = useState(false); const cardsToShow = useMemo(() => { if (shouldDisplayCardDomain) { return CardUtils.getDomainCards(cardList)[domain].filter((card) => !card.nameValuePairs?.issuedBy); } - return [cardList?.[cardId]]; - }, [shouldDisplayCardDomain, cardList, cardId, domain]); + return [cardList?.[cardID]]; + }, [shouldDisplayCardDomain, cardList, cardID, domain]); useEffect(() => { setIsNotFound(!cardsToShow); }, [cardList, cardsToShow]); @@ -104,30 +104,30 @@ function ExpensifyCardPage({ const [isCardDetailsLoading, setIsCardDetailsLoading] = useState<Record<number, boolean>>({}); const [cardsDetailsErrors, setCardsDetailsErrors] = useState<Record<number, string>>({}); - const handleRevealDetails = (revealedCardId: number) => { + const handleRevealDetails = (revealedcardID: number) => { setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({ ...prevState, - [revealedCardId]: true, + [revealedcardID]: true, })); // We can't store the response in Onyx for security reasons. // That is why this action is handled manually and the response is stored in a local state // Hence eslint disable here. // eslint-disable-next-line rulesdir/no-thenable-actions-in-views - Card.revealVirtualCardDetails(revealedCardId) + Card.revealVirtualCardDetails(revealedcardID) .then((value) => { - setCardsDetails((prevState: Record<number, TCardDetails | null>) => ({...prevState, [revealedCardId]: value as TCardDetails})); + setCardsDetails((prevState: Record<number, TCardDetails | null>) => ({...prevState, [revealedcardID]: value as TCardDetails})); setCardsDetailsErrors((prevState) => ({ ...prevState, - [revealedCardId]: '', + [revealedcardID]: '', })); }) .catch((error) => { setCardsDetailsErrors((prevState) => ({ ...prevState, - [revealedCardId]: error, + [revealedcardID]: error, })); }) - .finally(() => setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({...prevState, [revealedCardId]: false}))); + .finally(() => setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({...prevState, [revealedcardID]: false}))); }; const hasDetectedDomainFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN); @@ -249,7 +249,7 @@ function ExpensifyCardPage({ titleStyle={styles.walletCardMenuItem} icon={Expensicons.Flag} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain, cardId))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain, cardID))} /> </> ))} @@ -269,7 +269,7 @@ function ExpensifyCardPage({ title={translate('reportCardLostOrDamaged.report')} icon={Expensicons.Flag} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain, cardId))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain, cardID))} /> </> ); @@ -282,7 +282,7 @@ function ExpensifyCardPage({ success large style={[styles.w100, styles.p5]} - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(domain, cardId))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(domain, cardID))} text={translate('activateCardPage.activatePhysicalCard')} /> )} diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index 991c63d1d760..a6196f8df212 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -76,14 +76,14 @@ function ReportCardLostPage({ }, cardList = {}, route: { - params: {domain = '', cardId = ''}, + params: {domain = '', cardID = ''}, }, formData, }: ReportCardLostPageProps) { const styles = useThemeStyles(); usePrivatePersonalDetails(); - const physicalCard = cardList?.[cardId]; + const physicalCard = cardList?.[cardID]; const {translate} = useLocalize(); @@ -101,8 +101,8 @@ function ReportCardLostPage({ return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId)); - }, [domain, formData?.isLoading, prevIsLoading, physicalCard?.errors, cardId]); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID)); + }, [domain, formData?.isLoading, prevIsLoading, physicalCard?.errors, cardID]); useEffect(() => { if (formData?.isLoading && isEmptyObject(physicalCard?.errors)) { diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx index 1706c9bef510..9221e82a1d3e 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx @@ -34,7 +34,7 @@ type ReportVirtualCardFraudPageProps = ReportVirtualCardFraudPageOnyxProps & Sta function ReportVirtualCardFraudPage({ route: { - params: {domain = '', cardId = ''}, + params: {domain = '', cardID = ''}, }, cardList, formData, @@ -42,7 +42,7 @@ function ReportVirtualCardFraudPage({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const virtualCard = cardList?.[cardId]; + const virtualCard = cardList?.[cardID]; const virtualCardError = ErrorUtils.getLatestErrorMessage(virtualCard?.errors ?? {}); const prevIsLoading = usePrevious(formData?.isLoading); @@ -55,8 +55,8 @@ function ReportVirtualCardFraudPage({ return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId)); - }, [cardId, domain, formData?.isLoading, prevIsLoading, virtualCard?.errors]); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID)); + }, [cardID, domain, formData?.isLoading, prevIsLoading, virtualCard?.errors]); if (isEmptyObject(virtualCard)) { return <NotFoundPage />; @@ -66,7 +66,7 @@ function ReportVirtualCardFraudPage({ <ScreenWrapper testID={ReportVirtualCardFraudPage.displayName}> <HeaderWithBackButton title={translate('reportFraudPage.title')} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardId))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID))} /> <View style={[styles.flex1, styles.justifyContentBetween]}> <Text style={[styles.webViewStyles.baseFontStyle, styles.mh5]}>{translate('reportFraudPage.description')}</Text> From f19d447fffee65435b173d43a31bb6fe0cf5c637 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Fri, 12 Apr 2024 20:37:38 +0200 Subject: [PATCH 087/580] refactor: wip --- .../useReimbursementAccountStepFormSubmit.ts | 4 +- .../PersonalInfo/PersonalInfo.tsx | 35 +++++++++------- .../PersonalInfo/substeps/Address.tsx | 42 +++++++++---------- .../PersonalInfo/substeps/Confirmation.tsx | 29 +++++++------ .../PersonalInfo/substeps/DateOfBirth.tsx | 37 +++++++--------- .../PersonalInfo/substeps/FullName.tsx | 23 +++++----- .../substeps/SocialSecurityNumber.tsx | 21 +++++----- .../EnablePayments/utils/getSubstepValues.ts | 19 +++++++++ src/types/form/PersonalBankAccountForm.ts | 8 ++-- src/types/onyx/WalletAdditionalDetails.ts | 8 ++++ 10 files changed, 127 insertions(+), 99 deletions(-) create mode 100644 src/pages/EnablePayments/utils/getSubstepValues.ts diff --git a/src/hooks/useReimbursementAccountStepFormSubmit.ts b/src/hooks/useReimbursementAccountStepFormSubmit.ts index 98f079dd2447..85a9b21b4d8d 100644 --- a/src/hooks/useReimbursementAccountStepFormSubmit.ts +++ b/src/hooks/useReimbursementAccountStepFormSubmit.ts @@ -7,7 +7,7 @@ import type {SubStepProps} from './useSubStep/types'; type UseReimbursementAccountStepFormSubmitParams = Pick<SubStepProps, 'onNext'> & { formId?: OnyxFormKey; - fieldIds: Array<FormOnyxKeys<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>>; + fieldIds: Array<FormOnyxKeys<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM | typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>>; shouldSaveDraft: boolean; }; @@ -26,7 +26,7 @@ export default function useReimbursementAccountStepFormSubmit({ shouldSaveDraft, }: UseReimbursementAccountStepFormSubmitParams) { return useCallback( - (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>) => { + (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM | typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>) => { if (shouldSaveDraft) { const stepValues = fieldIds.reduce( (acc, key) => ({ diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 43c02e6c104f..ea97d8cfcaa3 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -2,15 +2,13 @@ import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useSubStep from '@hooks/useSubStep'; -import {SubStepProps} from '@hooks/useSubStep/types'; -import useTheme from '@hooks/useTheme'; +import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import getInitialSubstepForPersonalInfo from '@pages/ReimbursementAccount/utils/getInitialSubstepForPersonalInfo'; @@ -18,9 +16,9 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import type {UserWallet} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import INPUT_IDS, {PersonalBankAccountForm} from '@src/types/form/PersonalBankAccountForm'; +import type {UserWallet, WalletAdditionalDetails} from '@src/types/onyx'; +import getSubstepValues from '../utils/getSubstepValues'; import Address from './substeps/Address'; import Confirmation from './substeps/Confirmation'; import DateOfBirth from './substeps/DateOfBirth'; @@ -30,6 +28,12 @@ import SocialSecurityNumber from './substeps/SocialSecurityNumber'; type EnablePaymentsPageOnyxProps = { /** The user's wallet */ userWallet: OnyxEntry<UserWallet>; + + /** Reimbursement account from ONYX */ + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; + + /** The draft values of the bank account being setup */ + walletAdditionalDetailsDraft: OnyxEntry<PersonalBankAccountForm>; }; type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps; @@ -37,7 +41,7 @@ type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, SocialSecurityNumber, Address, Confirmation]; -function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { +function EnablePaymentsPage({userWallet, walletAdditionalDetails, walletAdditionalDetailsDraft}: EnablePaymentsPageProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); @@ -46,7 +50,7 @@ function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { const submit = () => {}; - const values = {}; + const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, walletAdditionalDetailsDraft, walletAdditionalDetails), [walletAdditionalDetails, walletAdditionalDetailsDraft]); useEffect(() => { if (isOffline) { @@ -64,11 +68,7 @@ function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(values), [values]); - const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo, goToTheLastStep} = useSubStep({bodyContent, startFrom, onFinished: submit}); - - if (isEmptyObject(userWallet)) { - return <FullScreenLoadingIndicator />; - } + const {componentToRender: SubStep, isEditing, nextScreen, prevScreen, moveTo} = useSubStep({bodyContent, startFrom, onFinished: submit}); return ( <ScreenWrapper @@ -78,7 +78,7 @@ function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { > <HeaderWithBackButton title={translate('personalInfoStep.personalInfo')} - onBackButtonPress={() => Navigation.navigate(ROUTES.SETTINGS)} + onBackButtonPress={prevScreen} /> <View style={[styles.ph5, styles.mb5, styles.mt3, {height: CONST.BANK_ACCOUNT.STEPS_HEADER_HEIGHT}]}> <InteractiveStepSubHeader @@ -105,4 +105,11 @@ export default withOnyx<EnablePaymentsPageProps, EnablePaymentsPageOnyxProps>({ // stored values here. initWithStoredValues: false, }, + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + walletAdditionalDetails: { + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + }, + walletAdditionalDetailsDraft: { + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, + }, })(EnablePaymentsPage); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx index 712661510b7d..445cf11282b1 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx @@ -10,15 +10,15 @@ import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccoun import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ValidationUtils from '@libs/ValidationUtils'; -import AddressForm from '@pages/ReimbursementAccount/AddressForm'; +import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import type {ReimbursementAccount} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {WalletAdditionalDetails} from '@src/types/onyx'; type AddressOnyxProps = { - /** Reimbursement account from ONYX */ - reimbursementAccount: OnyxEntry<ReimbursementAccount>; + /** wallet additional details from ONYX */ + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; }; type AddressProps = AddressOnyxProps & SubStepProps; @@ -34,32 +34,33 @@ const INPUT_KEYS = { const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.STREET, PERSONAL_INFO_STEP_KEY.CITY, PERSONAL_INFO_STEP_KEY.STATE, PERSONAL_INFO_STEP_KEY.ZIP_CODE]; -const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - if (values.requestorAddressStreet && !ValidationUtils.isValidAddress(values.requestorAddressStreet)) { - errors.requestorAddressStreet = 'bankAccount.error.addressStreet'; + if (values.addressStreet && !ValidationUtils.isValidAddress(values.addressStreet)) { + errors.addressStreet = 'bankAccount.error.addressStreet'; } - if (values.requestorAddressZipCode && !ValidationUtils.isValidZipCode(values.requestorAddressZipCode)) { - errors.requestorAddressZipCode = 'bankAccount.error.zipCode'; + if (values.addressZipCode && !ValidationUtils.isValidZipCode(values.addressZipCode)) { + errors.addressZipCode = 'bankAccount.error.zipCode'; } return errors; }; -function Address({reimbursementAccount, onNext, isEditing}: AddressProps) { +function Address({walletAdditionalDetails, onNext, isEditing}: AddressProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const defaultValues = { - street: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.STREET] ?? '', - city: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.CITY] ?? '', - state: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.STATE] ?? '', - zipCode: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.ZIP_CODE] ?? '', + street: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.STREET] ?? '', + city: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.CITY] ?? '', + state: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.STATE] ?? '', + zipCode: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.ZIP_CODE] ?? '', }; const handleSubmit = useReimbursementAccountStepFormSubmit({ + formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, @@ -67,7 +68,7 @@ function Address({reimbursementAccount, onNext, isEditing}: AddressProps) { return ( <FormProvider - formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + formID={ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS} submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} validate={validate} onSubmit={handleSubmit} @@ -77,9 +78,8 @@ function Address({reimbursementAccount, onNext, isEditing}: AddressProps) { <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.enterYourAddress')}</Text> <Text style={[styles.textSupporting]}>{translate('common.noPO')}</Text> - <AddressForm + <AddressFormFields inputKeys={INPUT_KEYS} - translate={translate} streetTranslationKey="common.streetAddress" defaultValues={defaultValues} shouldSaveDraft={!isEditing} @@ -93,8 +93,8 @@ function Address({reimbursementAccount, onNext, isEditing}: AddressProps) { Address.displayName = 'Address'; export default withOnyx<AddressProps, AddressOnyxProps>({ - // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + walletAdditionalDetails: { + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, })(Address); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx index 0e11aff395de..866cc30611a2 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx @@ -17,16 +17,15 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReimbursementAccountForm} from '@src/types/form'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import type {ReimbursementAccount} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {WalletAdditionalDetails} from '@src/types/onyx'; type ConfirmationOnyxProps = { - /** Reimbursement account from ONYX */ - reimbursementAccount: OnyxEntry<ReimbursementAccount>; + /** wallet additional details from ONYX */ + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; /** The draft values of the bank account being setup */ - reimbursementAccountDraft: OnyxEntry<ReimbursementAccountForm>; + walletAdditionalDetailsDraft: OnyxEntry<WalletAdditionalDetails>; }; type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; @@ -34,14 +33,14 @@ type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; const PERSONAL_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX.PERSONAL_INFO; -function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext, onMove}: ConfirmationProps) { +function Confirmation({walletAdditionalDetails, walletAdditionalDetailsDraft, onNext, onMove}: ConfirmationProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); - const isLoading = reimbursementAccount?.isLoading ?? false; - const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); - const error = ErrorUtils.getLatestErrorMessage(reimbursementAccount ?? {}); + const isLoading = walletAdditionalDetailsDraft?.isLoading ?? false; + const error = ErrorUtils.getLatestErrorMessage(walletAdditionalDetails ?? {}); + const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, walletAdditionalDetailsDraft, walletAdditionalDetails), [walletAdditionalDetails, walletAdditionalDetailsDraft]); return ( <SafeAreaConsumer> @@ -136,11 +135,11 @@ function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext, Confirmation.displayName = 'Confirmation'; export default withOnyx<ConfirmationProps, ConfirmationOnyxProps>({ - // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + walletAdditionalDetails: { + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, - reimbursementAccountDraft: { - key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, + walletAdditionalDetailsDraft: { + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, }, })(Confirmation); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx index b02a56403641..a42b9c76d1a9 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx @@ -15,16 +15,12 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReimbursementAccountForm} from '@src/types/form'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import type {ReimbursementAccount} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {WalletAdditionalDetails} from '@src/types/onyx'; type DateOfBirthOnyxProps = { /** Reimbursement account from ONYX */ - reimbursementAccount: OnyxEntry<ReimbursementAccount>; - - /** The draft values of the bank account being setup */ - reimbursementAccountDraft: OnyxEntry<ReimbursementAccountForm>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; }; type DateOfBirthProps = DateOfBirthOnyxProps & SubStepProps; @@ -32,7 +28,7 @@ type DateOfBirthProps = DateOfBirthOnyxProps & SubStepProps; const PERSONAL_INFO_DOB_KEY = INPUT_IDS.PERSONAL_INFO_STEP.DOB; const STEP_FIELDS = [PERSONAL_INFO_DOB_KEY]; -const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); if (values.dob) { @@ -46,16 +42,17 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACC return errors; }; -function DateOfBirth({reimbursementAccount, reimbursementAccountDraft, onNext, isEditing}: DateOfBirthProps) { +const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); +const maxDate = subYears(new Date(), CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); + +function DateOfBirth({walletAdditionalDetails, onNext, isEditing}: DateOfBirthProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const dobDefaultValue = reimbursementAccount?.achData?.[PERSONAL_INFO_DOB_KEY] ?? reimbursementAccountDraft?.[PERSONAL_INFO_DOB_KEY] ?? ''; - - const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); - const maxDate = subYears(new Date(), CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); + const dobDefaultValue = walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? ''; const handleSubmit = useReimbursementAccountStepFormSubmit({ + formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, @@ -63,7 +60,7 @@ function DateOfBirth({reimbursementAccount, reimbursementAccountDraft, onNext, i return ( <FormProvider - formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + formID={ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS} submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} validate={validate} onSubmit={handleSubmit} @@ -71,8 +68,7 @@ function DateOfBirth({reimbursementAccount, reimbursementAccountDraft, onNext, i submitButtonStyles={[styles.pb5, styles.mb0]} > <Text style={[styles.textHeadlineLineHeightXXL, styles.mb5]}>{translate('personalInfoStep.enterYourDateOfBirth')}</Text> - {/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */} - <InputWrapper<unknown> + <InputWrapper InputComponent={DatePicker} inputID={PERSONAL_INFO_DOB_KEY} label={translate('common.dob')} @@ -90,11 +86,8 @@ function DateOfBirth({reimbursementAccount, reimbursementAccountDraft, onNext, i DateOfBirth.displayName = 'DateOfBirth'; export default withOnyx<DateOfBirthProps, DateOfBirthOnyxProps>({ - // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - }, - reimbursementAccountDraft: { - key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + walletAdditionalDetails: { + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, })(DateOfBirth); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx index 1d225c1c32f2..89b60b1ba390 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx @@ -15,12 +15,12 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import type {ReimbursementAccount} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {WalletAdditionalDetails} from '@src/types/onyx'; type FullNameOnyxProps = { - /** Reimbursement account from ONYX */ - reimbursementAccount: OnyxEntry<ReimbursementAccount>; + /** Wallet Additional Details from ONYX */ + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; }; type FullNameProps = FullNameOnyxProps & SubStepProps; @@ -28,7 +28,7 @@ type FullNameProps = FullNameOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME]; -const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); if (values.firstName && !ValidationUtils.isValidLegalName(values.firstName)) { errors.firstName = 'bankAccount.error.firstName'; @@ -40,16 +40,17 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACC return errors; }; -function FullName({reimbursementAccount, onNext, isEditing}: FullNameProps) { +function FullName({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const defaultValues = { - firstName: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.FIRST_NAME] ?? '', - lastName: reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', + firstName: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.FIRST_NAME] ?? '', + lastName: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', }; const handleSubmit = useReimbursementAccountStepFormSubmit({ + formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, @@ -57,7 +58,7 @@ function FullName({reimbursementAccount, onNext, isEditing}: FullNameProps) { return ( <FormProvider - formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + formID={ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS} submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} validate={validate} onSubmit={handleSubmit} @@ -98,7 +99,7 @@ FullName.displayName = 'FullName'; export default withOnyx<FullNameProps, FullNameOnyxProps>({ // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + walletAdditionalDetails: { + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, })(FullName); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx index e647fd768fb1..4e82fc3570ed 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx @@ -15,12 +15,12 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import type {ReimbursementAccount} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {WalletAdditionalDetails} from '@src/types/onyx'; type SocialSecurityNumberOnyxProps = { /** Reimbursement account from ONYX */ - reimbursementAccount: OnyxEntry<ReimbursementAccount>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; }; type SocialSecurityNumberProps = SocialSecurityNumberOnyxProps & SubStepProps; @@ -28,7 +28,7 @@ type SocialSecurityNumberProps = SocialSecurityNumberOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; -const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { @@ -37,13 +37,14 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACC return errors; }; -function SocialSecurityNumber({reimbursementAccount, onNext, isEditing}: SocialSecurityNumberProps) { +function SocialSecurityNumber({walletAdditionalDetails, onNext, isEditing}: SocialSecurityNumberProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const defaultSsnLast4 = reimbursementAccount?.achData?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? ''; + const defaultSsnLast4 = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? ''; const handleSubmit = useReimbursementAccountStepFormSubmit({ + formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, @@ -51,7 +52,7 @@ function SocialSecurityNumber({reimbursementAccount, onNext, isEditing}: SocialS return ( <FormProvider - formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + formID={ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS} submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} validate={validate} onSubmit={handleSubmit} @@ -84,8 +85,8 @@ function SocialSecurityNumber({reimbursementAccount, onNext, isEditing}: SocialS SocialSecurityNumber.displayName = 'SocialSecurityNumber'; export default withOnyx<SocialSecurityNumberProps, SocialSecurityNumberOnyxProps>({ - // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + walletAdditionalDetails: { + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, })(SocialSecurityNumber); diff --git a/src/pages/EnablePayments/utils/getSubstepValues.ts b/src/pages/EnablePayments/utils/getSubstepValues.ts new file mode 100644 index 000000000000..d78c081197c9 --- /dev/null +++ b/src/pages/EnablePayments/utils/getSubstepValues.ts @@ -0,0 +1,19 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {PersonalBankAccountForm} from '@src/types/form'; +import type {WalletAdditionalDetails} from '@src/types/onyx'; + +function getSubstepValues<T extends keyof PersonalBankAccountForm>( + inputKeys: Record<string, T>, + walletAdditionalDetailsDraft: OnyxEntry<PersonalBankAccountForm>, + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>, +): {[K in T]: PersonalBankAccountForm[K]} { + return Object.entries(inputKeys).reduce( + (acc, [, value]) => ({ + ...acc, + [value]: walletAdditionalDetailsDraft?.[value] ?? walletAdditionalDetails?.[value] ?? '', + }), + {} as {[K in T]: PersonalBankAccountForm[K]}, + ); +} + +export default getSubstepValues; diff --git a/src/types/form/PersonalBankAccountForm.ts b/src/types/form/PersonalBankAccountForm.ts index c4bdfc7778d2..c07c62663567 100644 --- a/src/types/form/PersonalBankAccountForm.ts +++ b/src/types/form/PersonalBankAccountForm.ts @@ -18,10 +18,10 @@ const INPUT_IDS = { LAST_NAME: 'lastName', DOB: 'dob', SSN_LAST_4: 'ssnLast4', - STREET: 'requestorAddressStreet', - CITY: 'requestorAddressCity', - STATE: 'requestorAddressState', - ZIP_CODE: 'requestorAddressZipCode', + STREET: 'addressStreet', + CITY: 'addressCity', + STATE: 'addressState', + ZIP_CODE: 'addressZipCode', IS_ONFIDO_SETUP_COMPLETE: 'isOnfidoSetupComplete', }, } as const; diff --git a/src/types/onyx/WalletAdditionalDetails.ts b/src/types/onyx/WalletAdditionalDetails.ts index b5521f2e79c8..4ed1cd1f2ed7 100644 --- a/src/types/onyx/WalletAdditionalDetails.ts +++ b/src/types/onyx/WalletAdditionalDetails.ts @@ -21,6 +21,14 @@ type WalletAdditionalDetails = { additionalErrorMessage?: string; isLoading?: boolean; errors?: OnyxCommon.Errors; + firstName: string; + lastName: string; + dob: string; + ssnLast4: string; + addressStreet: string; + addressCity: string; + addressState: string; + addressZipCode: string; }; export default WalletAdditionalDetails; From 78b79ccc004a4388d8d38908cd9b611b6330c63a Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Sun, 14 Apr 2024 16:05:43 +0530 Subject: [PATCH 088/580] allow admin & approver to update receipt when iou request is open. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/libs/ReportUtils.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b03abbcdf9bb..5df30c74d255 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2428,7 +2428,7 @@ function getTransactionDetails(transaction: OnyxEntry<Transaction>, createdDateF * This is used in conjunction with canEditRestrictedField to control editing of specific fields like amount, currency, created, receipt, and distance. * On its own, it only controls allowing/disallowing navigating to the editing pages or showing/hiding the 'Edit' icon on report actions */ -function canEditMoneyRequest(reportAction: OnyxEntry<ReportAction>): boolean { +function canEditMoneyRequest(reportAction: OnyxEntry<ReportAction>, fieldToEdit?: ValueOf<typeof CONST.EDIT_REQUEST_FIELD>): boolean { const isDeleted = ReportActionsUtils.isDeletedAction(reportAction); if (isDeleted) { @@ -2466,6 +2466,10 @@ function canEditMoneyRequest(reportAction: OnyxEntry<ReportAction>): boolean { const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; const isManager = currentUserAccountID === moneyRequestReport?.managerID; + if ((isAdmin || isManager) && ((isOpenExpenseReport(moneyRequestReport) && fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) || isRequestor)) { + return true; + } + // Admin & managers can always edit coding fields such as tag, category, billable, etc. As long as the report has a state higher than OPEN. if ((isAdmin || isManager) && !isOpenExpenseReport(moneyRequestReport)) { return true; @@ -2489,7 +2493,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry<ReportAction>, field CONST.EDIT_REQUEST_FIELD.DISTANCE, ]; - if (!canEditMoneyRequest(reportAction)) { + if (!canEditMoneyRequest(reportAction, fieldToEdit)) { return false; } @@ -2521,8 +2525,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry<ReportAction>, field } if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { - const isRequestor = currentUserAccountID === reportAction?.actorAccountID; - return !TransactionUtils.isReceiptBeingScanned(transaction) && !TransactionUtils.isDistanceRequest(transaction) && isRequestor; + return !TransactionUtils.isReceiptBeingScanned(transaction) && !TransactionUtils.isDistanceRequest(transaction); } return true; From 87270fbb8ef98c9465f8ff2c8ec7699378f1f42e Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Sun, 14 Apr 2024 19:32:48 +0530 Subject: [PATCH 089/580] revert: allow admin & approver to update receipt when iou request is open. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/libs/ReportUtils.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5df30c74d255..b03abbcdf9bb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2428,7 +2428,7 @@ function getTransactionDetails(transaction: OnyxEntry<Transaction>, createdDateF * This is used in conjunction with canEditRestrictedField to control editing of specific fields like amount, currency, created, receipt, and distance. * On its own, it only controls allowing/disallowing navigating to the editing pages or showing/hiding the 'Edit' icon on report actions */ -function canEditMoneyRequest(reportAction: OnyxEntry<ReportAction>, fieldToEdit?: ValueOf<typeof CONST.EDIT_REQUEST_FIELD>): boolean { +function canEditMoneyRequest(reportAction: OnyxEntry<ReportAction>): boolean { const isDeleted = ReportActionsUtils.isDeletedAction(reportAction); if (isDeleted) { @@ -2466,10 +2466,6 @@ function canEditMoneyRequest(reportAction: OnyxEntry<ReportAction>, fieldToEdit? const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; const isManager = currentUserAccountID === moneyRequestReport?.managerID; - if ((isAdmin || isManager) && ((isOpenExpenseReport(moneyRequestReport) && fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) || isRequestor)) { - return true; - } - // Admin & managers can always edit coding fields such as tag, category, billable, etc. As long as the report has a state higher than OPEN. if ((isAdmin || isManager) && !isOpenExpenseReport(moneyRequestReport)) { return true; @@ -2493,7 +2489,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry<ReportAction>, field CONST.EDIT_REQUEST_FIELD.DISTANCE, ]; - if (!canEditMoneyRequest(reportAction, fieldToEdit)) { + if (!canEditMoneyRequest(reportAction)) { return false; } @@ -2525,7 +2521,8 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry<ReportAction>, field } if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { - return !TransactionUtils.isReceiptBeingScanned(transaction) && !TransactionUtils.isDistanceRequest(transaction); + const isRequestor = currentUserAccountID === reportAction?.actorAccountID; + return !TransactionUtils.isReceiptBeingScanned(transaction) && !TransactionUtils.isDistanceRequest(transaction) && isRequestor; } return true; From ae3bb23ddc248bf0626448451a9c017b9fd0bbf0 Mon Sep 17 00:00:00 2001 From: dukenv0307 <dukenv0307@gmail.com> Date: Mon, 15 Apr 2024 10:36:13 +0700 Subject: [PATCH 090/580] fix: Broken message text in LHN when Inviting someone to room --- src/libs/SidebarUtils.ts | 9 +-------- src/types/onyx/Report.ts | 3 ++- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index b487aacad987..fbc8f1361f03 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -323,15 +323,8 @@ function getOptionData({ const isThreadMessage = ReportUtils.isThread(report) && lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && lastAction?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - // Currently the back-end is not returning the `lastActionName` so I use this to mock, - // ideally it should be returned from the back-end in the `report`, like the `lastMessageText` and `lastMessageHtml` - if (report.lastMessageHtml && report.lastMessageHtml.includes('invited <mention-user')) { - report.lastActionName = CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM; - } - if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport || isThreadMessage) && !result.isArchivedRoom) { - - const lastActionName = lastAction?.actionName ?? report.lastActionName; + const lastActionName = lastAction?.actionName ?? report.lastActionType; if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { const newName = lastAction?.originalMessage?.newName ?? ''; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 7e39e7f65a22..280faf23de5e 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -137,6 +137,8 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< displayName?: string; lastMessageHtml?: string; lastActorAccountID?: number; + // indicate the type of the last action + lastActionType?: typeof CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG; ownerAccountID?: number; ownerEmail?: string; participants?: Participants; @@ -149,7 +151,6 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< managerEmail?: string; parentReportActionIDs?: number[]; errorFields?: OnyxCommon.ErrorFields; - /** Whether the report is waiting on a bank account */ isWaitingOnBankAccount?: boolean; From 2001062d0b83ebd4138873ad22130fa67cd2be11 Mon Sep 17 00:00:00 2001 From: dukenv0307 <dukenv0307@gmail.com> Date: Mon, 15 Apr 2024 11:32:33 +0700 Subject: [PATCH 091/580] fix: typecheck --- ios/Podfile.lock | 4 ++-- src/libs/SidebarUtils.ts | 7 +++++-- src/types/onyx/Report.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 231ce0248d5e..fb15b1af32ad 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2582,8 +2582,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d -COCOAPODS: 1.13.0 +COCOAPODS: 1.14.3 diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index fbc8f1361f03..de8c86d38c8c 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -12,6 +12,7 @@ import type Report from '@src/types/onyx/Report'; import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type { ChangeLog } from '@src/types/onyx/OriginalMessage'; import * as CollectionUtils from './CollectionUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; import localeCompare from './LocaleCompare'; @@ -337,7 +338,9 @@ function getOptionData({ lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM || lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.REMOVE_FROM_ROOM ) { - const targetAccountIDsLength = lastAction?.originalMessage?.targetAccountIDs?.length ?? report.lastMessageHtml?.match(/<mention-user[^>]*><\/mention-user>/g)?.length ?? []; + const lastActionOriginalMessage = lastAction.actionName ? lastAction?.originalMessage as ChangeLog : null ; + const targetAccountIDs = lastActionOriginalMessage?.targetAccountIDs ?? [] + const targetAccountIDsLength = targetAccountIDs.length ?? report.lastMessageHtml?.match(/<mention-user[^>]*><\/mention-user>/g)?.length ?? 0; const verb = lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM ? Localize.translate(preferredLocale, 'workspace.invite.invited') @@ -345,7 +348,7 @@ function getOptionData({ const users = Localize.translate(preferredLocale, targetAccountIDsLength > 1 ? 'workspace.invite.users' : 'workspace.invite.user'); result.alternateText = `${lastActorDisplayName} ${verb} ${targetAccountIDsLength} ${users}`.trim(); - const roomName = lastAction?.originalMessage?.roomName ?? ''; + const roomName = lastActionOriginalMessage?.roomName ?? ''; if (roomName) { const preposition = lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 280faf23de5e..f53eecb82fa0 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -138,7 +138,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< lastMessageHtml?: string; lastActorAccountID?: number; // indicate the type of the last action - lastActionType?: typeof CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG; + lastActionType?: ValueOf<typeof CONST.REPORT.ACTIONS.TYPE>; ownerAccountID?: number; ownerEmail?: string; participants?: Participants; From 535ffbbbb440dba07cc1806cbb37057c5ca1776b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Mon, 15 Apr 2024 10:54:42 +0200 Subject: [PATCH 092/580] Update Podfile --- ios/Podfile.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f9244c515a2c..2ec4f671c529 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1836,7 +1836,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.47): + - RNLiveMarkdown (0.1.53): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1854,9 +1854,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.47) + - RNLiveMarkdown/common (= 0.1.53) - Yoga - - RNLiveMarkdown/common (0.1.47): + - RNLiveMarkdown/common (0.1.53): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2565,7 +2565,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: f172c7199283dc9d21bccf7e21ea10741fd19e1d + RNLiveMarkdown: e49132a5e1b6e40fa4485f13f5d9846ae4936a93 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729 From 8895b172383f065056caac088f0505cfa01f8c74 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 15 Apr 2024 13:48:01 +0200 Subject: [PATCH 093/580] Minor lint fix --- src/pages/iou/request/IOURequestStartPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 7ee64ac059c8..ad9a95196e35 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -1,7 +1,7 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry, OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; From 7526db130edf7473253ade73bf425fd19dac5ed8 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 15 Apr 2024 14:09:07 +0200 Subject: [PATCH 094/580] Enable add receipt file feature, lint fixes --- ...raryForRefactorRequestConfirmationList.tsx | 42 +++++++++---------- .../step/IOURequestStepConfirmation.tsx | 21 +++++++++- .../request/step/IOURequestStepSendFrom.tsx | 4 +- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 5f9cea934508..4c422cee2db6 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -659,29 +659,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ - { - item: ( - <MenuItem - key={translate('workspace.invoices.sendFrom')} - shouldShowRightIcon={!isReadOnly && canUpdateSenderWorkspace} - title={senderWorkspace?.name} - icon={senderWorkspace?.avatar ? senderWorkspace?.avatar : getDefaultWorkspaceAvatar(senderWorkspace?.name)} - iconType={CONST.ICON_TYPE_WORKSPACE} - description={translate('workspace.common.workspace')} - label={translate('workspace.invoices.sendFrom')} - isLabelHoverable={false} - interactive={!isReadOnly && canUpdateSenderWorkspace} - onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); - }} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - disabled={didConfirm || !canUpdateSenderWorkspace} - /> - ), - shouldShow: isTypeInvoice, - isSupplementary: false, - }, { item: ( <MenuItemWithTopDescription @@ -1020,6 +997,25 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ <ConfirmedRoute transaction={transaction ?? ({} as OnyxTypes.Transaction)} /> </View> )} + {isTypeInvoice && ( + <MenuItem + key={translate('workspace.invoices.sendFrom')} + shouldShowRightIcon={!isReadOnly && canUpdateSenderWorkspace} + title={senderWorkspace?.name} + icon={senderWorkspace?.avatar ? senderWorkspace?.avatar : getDefaultWorkspaceAvatar(senderWorkspace?.name)} + iconType={CONST.ICON_TYPE_WORKSPACE} + description={translate('workspace.common.workspace')} + label={translate('workspace.invoices.sendFrom')} + isLabelHoverable={false} + interactive={!isReadOnly && canUpdateSenderWorkspace} + onPress={() => { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); + }} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + disabled={didConfirm || !canUpdateSenderWorkspace} + /> + )} { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing receiptImage || receiptThumbnail diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index be3fea36e7de..b48a869be87d 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -419,7 +419,21 @@ function IOURequestStepConfirmation({ requestMoney(selectedParticipants, trimmedComment); }, - [transaction, report, iouType, receiptFile, requestType, requestMoney, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, trackExpense, createDistanceRequest], + [ + transaction, + report, + iouType, + receiptFile, + requestType, + requestMoney, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + trackExpense, + createDistanceRequest, + policy, + policyTags, + policyCategories, + ], ); /** @@ -478,7 +492,10 @@ function IOURequestStepConfirmation({ <HeaderWithBackButton title={headerTitle} onBackButtonPress={navigateBack} - shouldShowThreeDotsButton={requestType === CONST.IOU.REQUEST_TYPE.MANUAL && (iouType === CONST.IOU.TYPE.REQUEST || iouType === CONST.IOU.TYPE.TRACK_EXPENSE)} + shouldShowThreeDotsButton={ + requestType === CONST.IOU.REQUEST_TYPE.MANUAL && + (iouType === CONST.IOU.TYPE.REQUEST || iouType === CONST.IOU.TYPE.TRACK_EXPENSE || iouType === CONST.IOU.TYPE.INVOICE) + } threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} threeDotsMenuItems={[ { diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index a90da053982b..da0448235036 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -59,7 +59,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte ], isSelected: !!transaction?.participants?.find((participant) => participant.policyID === policy.id), })); - }, [allPolicies, transaction, styles.mh2, styles.roundCheckmarkWrapper, theme.success]); + }, [allPolicies, transaction]); const navigateBack = () => { Navigation.goBack(backTo); @@ -88,7 +88,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte )} </View> ), - [workspaceOptions], + [styles.roundCheckmarkWrapper, styles.mh2, theme.success], ); return ( From e8f56c4efd1c9c07ba3b38a2c4665fd9e1afb4d7 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 15 Apr 2024 14:52:34 +0200 Subject: [PATCH 095/580] Minor UI fixes --- src/components/MenuItem.tsx | 8 ++++++-- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 3 ++- src/styles/index.ts | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index f23c8db97f47..98911f5152c8 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -87,6 +87,9 @@ type MenuItemBaseProps = { /** Any additional styles to apply on the badge element */ badgeStyle?: ViewStyle; + /** Any additional styles to apply to the label */ + labelStyle?: ViewStyle; + /** Any adjustments to style when menu item is hovered or pressed */ hoverAndPressStyle?: StyleProp<AnimatedStyle<ViewStyle>>; @@ -267,6 +270,7 @@ function MenuItem( outerWrapperStyle, containerStyle, titleStyle, + labelStyle, hoverAndPressStyle, descriptionTextStyle, badgeStyle, @@ -424,7 +428,7 @@ function MenuItem( return ( <View> {!!label && !isLabelHoverable && ( - <View style={[styles.ph5]}> + <View style={[styles.ph5, labelStyle]}> <Text style={StyleUtils.combineStyles([styles.sidebarLinkText, styles.optionAlternateText, styles.textLabelSupporting, styles.pre])}>{label}</Text> </View> )} @@ -460,7 +464,7 @@ function MenuItem( <> <View style={[styles.flexColumn, styles.flex1]}> {!!label && isLabelHoverable && ( - <View style={icon ? styles.mb2 : null}> + <View style={[icon ? styles.mb2 : null, labelStyle]}> <Text style={StyleUtils.combineStyles([styles.sidebarLinkText, styles.optionAlternateText, styles.textLabelSupporting, styles.pre])}> {label} </Text> diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 4c422cee2db6..10a93535ac53 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -1011,7 +1011,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ onPress={() => { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); }} - style={[styles.moneyRequestMenuItem]} + style={styles.moneyRequestMenuItem} + labelStyle={styles.mt2} titleStyle={styles.flex1} disabled={didConfirm || !canUpdateSenderWorkspace} /> diff --git a/src/styles/index.ts b/src/styles/index.ts index ebe10c4c26bd..e84e65dd6ea3 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3021,9 +3021,9 @@ const styles = (theme: ThemeColors) => roundCheckmarkWrapper: { alignItems: 'center', justifyContent: 'center', - height: 24, - width: 24, - borderRadius: 12, + height: 28, + width: 28, + borderRadius: 14, borderColor: theme.borderLighter, borderWidth: 1, }, From 48fa52e9685d90e68aed378e77871fcafa166679 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 15 Apr 2024 15:17:18 +0200 Subject: [PATCH 096/580] remove unnecessary pause --- src/libs/actions/OnyxUpdateManager.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 384ae00cf7e0..aa40ca702b9a 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -175,6 +175,7 @@ export default () => { Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, callback: (value: unknown) => { + // When there is no value or an invalid value, there's nothing to process, so let's return early. if (!isValidOnyxUpdateFromServer(value)) { return; } @@ -207,11 +208,6 @@ export default () => { // fully migrating to the reliable updates mode. // 2. This client already has the reliable updates mode enabled, but it's missing some updates and it // needs to fetch those. - // - // For both of those, we need to pause the sequential queue. This is important so that the updates are - // applied in their correct and specific order. If this queue was not paused, then there would be a lot of - // onyx data being applied while we are fetching the missing updates and that would put them all out of order. - SequentialQueue.pause(); // The flow below is setting the promise to a reconnect app to address flow (1) explained above. if (!lastUpdateIDAppliedToClient) { From beb16a3fa14c7e51707a5fa1a2b23843df5ff49d Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 15 Apr 2024 15:18:14 +0200 Subject: [PATCH 097/580] check for isLoadingApp and ActiveClientManager first --- src/libs/actions/OnyxUpdateManager.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index aa40ca702b9a..8714f4f6838c 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -175,11 +175,6 @@ export default () => { Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, callback: (value: unknown) => { - // When there is no value or an invalid value, there's nothing to process, so let's return early. - if (!isValidOnyxUpdateFromServer(value)) { - return; - } - // If isLoadingApp is positive it means that OpenApp command hasn't finished yet, and in that case // we don't have base state of the app (reports, policies, etc) setup. If we apply this update, // we'll only have them overriten by the openApp response. So let's skip it and return. @@ -197,6 +192,11 @@ export default () => { return; } + // When there is no value or an invalid value, there's nothing to process, so let's return early. + if (!isValidOnyxUpdateFromServer(value)) { + return; + } + const updateParams = value; const lastUpdateIDFromServer = value.lastUpdateID; const previousUpdateIDFromServer = value.previousUpdateID; From 66e6a31ea6a488ab6828db0eb8e21af42e10b8d7 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 15 Apr 2024 15:45:34 +0200 Subject: [PATCH 098/580] Fix translation --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 9207681526d5..df5e397df7bf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2172,7 +2172,7 @@ export default { unlockVBACopy: '¡Todo listo para recibir pagos por transferencia o con tarjeta!', viewUnpaidInvoices: 'Ver facturas emitidas pendientes', sendInvoice: 'Enviar factura', - sendFrom: 'Enviado desde', + sendFrom: 'Enviar desde', }, travel: { unlockConciergeBookingTravel: 'Desbloquea la reserva de viajes con Concierge', From 0c2b8027fcae3e07e47a668e596a774c615f2eb7 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 15 Apr 2024 17:10:42 +0200 Subject: [PATCH 099/580] Fix report preview message --- src/libs/ReportUtils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b25554244d5f..f19cf9468268 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -679,6 +679,13 @@ function isChatReport(report: OnyxEntry<Report> | EmptyObject): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } +/** + * Checks if a report is an invoice report. + */ +function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { + return report?.type === CONST.REPORT.TYPE.INVOICE; +} + /** * Checks if a report is an Expense report. */ @@ -2185,7 +2192,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea function getMoneyRequestSpendBreakdown(report: OnyxEntry<Report>, allReportsDict: OnyxCollection<Report> = null): SpendBreakdown { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; - if (isMoneyRequestReport(report)) { + if (isMoneyRequestReport(report) || isInvoiceReport(report)) { moneyRequestReport = report; } if (allAvailableReports && report?.iouReportID) { @@ -6227,6 +6234,7 @@ export { buildOptimisticInviteReportAction, isInvoiceRoom, getInvoiceChatByParticipants, + isInvoiceReport, }; export type { From 9731697f372d94facb58f838a514a0db04d88458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Mon, 15 Apr 2024 17:28:34 +0200 Subject: [PATCH 100/580] Fix composer collapsing --- ios/Podfile.lock | 8 ++++---- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2ec4f671c529..5adf6fc47cd6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1836,7 +1836,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.53): + - RNLiveMarkdown (0.1.61): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1854,9 +1854,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.53) + - RNLiveMarkdown/common (= 0.1.61) - Yoga - - RNLiveMarkdown/common (0.1.53): + - RNLiveMarkdown/common (0.1.61): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2565,7 +2565,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: e49132a5e1b6e40fa4485f13f5d9846ae4936a93 + RNLiveMarkdown: feecddf153470be3d8b1207970cd51df19be6778 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729 diff --git a/package-lock.json b/package-lock.json index f6dd8c79bff2..ea21355ea132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.53", + "@expensify/react-native-live-markdown": "0.1.61", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -3570,9 +3570,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.53", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.53.tgz", - "integrity": "sha512-E4nn3v2zz+0C9eLJfqOeJFebDHovvrNiSEgSbe3thFVgV25lTfPEJiuIXP1IT/zwzp0UadhDNhUGOV3m0ISNkA==", + "version": "0.1.61", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.61.tgz", + "integrity": "sha512-WGLN20P1OFkICtdjZYdMS6xFKUHDhdBETtIY9ReBTH43nZP1HiXWCivSz9+TewG/bZmxC95PmUaLwhwJ7sk1xQ==", "engines": { "node": ">= 18.0.0" }, diff --git a/package.json b/package.json index df95665c0b34..c39b6c7a2d26 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.53", + "@expensify/react-native-live-markdown": "0.1.61", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", From 326ef862cfbb654d7cf3d438b46ddf63264cb6cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Mon, 15 Apr 2024 17:39:28 +0200 Subject: [PATCH 101/580] Change text input width updating --- src/components/Composer/index.tsx | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 80aed2cb3e4f..a8737fdac47a 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -14,7 +14,6 @@ import useMarkdownStyle from '@hooks/useMarkdownStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; @@ -57,7 +56,6 @@ function Composer( style, shouldClear = false, autoFocus = false, - isFullComposerAvailable = false, shouldCalculateCaretPosition = false, isDisabled = false, onClear = () => {}, @@ -80,7 +78,6 @@ function Composer( const styles = useThemeStyles(); const markdownStyle = useMarkdownStyle(value); const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); const textRef = useRef<HTMLElement & RNText>(null); const textInput = useRef<AnimatedMarkdownTextInputRef | null>(null); const [selection, setSelection] = useState< @@ -230,22 +227,6 @@ function Composer( [onPasteFile, checkComposerVisibility], ); - /** - * Check the current text input width and update the state with the new width. - */ - const updateTextInputWidth = useCallback(() => { - if (!textInput.current) { - return; - } - const computedStyle = window.getComputedStyle(textInput.current); - setTextInputWidth(computedStyle.width); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, maxLines, isFullComposerAvailable, setIsFullComposerAvailable, windowWidth]); - - useEffect(() => { - updateTextInputWidth(); - }, [updateTextInputWidth]); - useEffect(() => { if (!textInput.current) { return; @@ -361,7 +342,10 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} onSelectionChange={addCursorPositionToSelectionChange} - onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles)} + onContentSizeChange={(e) => { + setTextInputWidth(`${e.nativeEvent.contentSize.width}px`); + ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); + }} disabled={isDisabled} onKeyPress={handleKeyPress} onFocus={(e) => { From f7d00db630e528c677e22972c5e839c004a037f2 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Mon, 15 Apr 2024 23:10:57 +0700 Subject: [PATCH 102/580] fix: workspace icon color in workspace switcher --- .../UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx | 1 + src/components/UserDetailsTooltip/types.tsx | 3 +++ src/pages/WorkspaceSwitcherPage.tsx | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx index 592cec3beca5..fa301167a230 100644 --- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx +++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx @@ -59,6 +59,7 @@ function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateA type={icon?.type ?? CONST.ICON_TYPE_AVATAR} name={icon?.name ?? userLogin} fallbackIcon={icon?.fallbackIcon} + iconID={icon?.id} /> </View> <Text style={[styles.mt2, styles.textMicroBold, styles.textReactionSenders, styles.textAlignCenter]}>{title}</Text> diff --git a/src/components/UserDetailsTooltip/types.tsx b/src/components/UserDetailsTooltip/types.tsx index b362c44877a9..7a072d4aa8a9 100644 --- a/src/components/UserDetailsTooltip/types.tsx +++ b/src/components/UserDetailsTooltip/types.tsx @@ -30,6 +30,9 @@ type Icon = { /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; + + /** ID of the Icon */ + id?: string; }; type UserDetailsTooltipProps = ChildrenProps & { diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 2c947af3c2d8..2bd1ebec2a3c 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -131,8 +131,9 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { { source: policy?.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy?.name), fallbackIcon: Expensicons.FallbackWorkspaceAvatar, - name: policy?.id, + name: policy?.name, type: CONST.ICON_TYPE_WORKSPACE, + id: policy?.id, }, ], boldStyle: hasUnreadData(policy?.id), From 9b2fa9420aa9e3359eb4b4207a2f368f250d7b00 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:17:00 +0530 Subject: [PATCH 103/580] Update --- .eslintrc.js | 1 + package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c2198da60c52..21467e061433 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -123,6 +123,7 @@ module.exports = { }, }, ], + 'rulesdir/avoid-anonymous-functions': 'off', }, }, // This helps disable the `prefer-alias` rule to be enabled for specific directories diff --git a/package-lock.json b/package-lock.json index 6013fb5e4dc7..83e1859c889f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -207,7 +207,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.45", + "eslint-config-expensify": "^2.0.46", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", @@ -19245,7 +19245,7 @@ } }, "node_modules/eslint-config-expensify": { - "version": "2.0.45", + "version": "2.0.46", "dev": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index ab699b006a35..cb27f981084c 100644 --- a/package.json +++ b/package.json @@ -258,7 +258,7 @@ "electron-builder": "24.13.2", "eslint": "^7.6.0", "eslint-config-airbnb-typescript": "^17.1.0", - "eslint-config-expensify": "^2.0.45", + "eslint-config-expensify": "^2.0.46", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^24.1.0", From c4ec0f7db21d5747caaa622305654bf5148d906f Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Tue, 16 Apr 2024 10:20:20 +0700 Subject: [PATCH 104/580] fix typescript --- src/components/UserDetailsTooltip/types.tsx | 23 ++----------------- .../home/report/ReportActionItemFragment.tsx | 3 +-- src/types/onyx/OnyxCommon.ts | 4 ++-- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/components/UserDetailsTooltip/types.tsx b/src/components/UserDetailsTooltip/types.tsx index 7a072d4aa8a9..07c9310810be 100644 --- a/src/components/UserDetailsTooltip/types.tsx +++ b/src/components/UserDetailsTooltip/types.tsx @@ -1,5 +1,5 @@ import type {AvatarSource} from '@libs/UserUtils'; -import type {AvatarType} from '@src/types/onyx/OnyxCommon'; +import type {AvatarType, Icon as IconType} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; type FallbackUserDetails = { @@ -16,25 +16,6 @@ type FallbackUserDetails = { type?: AvatarType; }; -type Icon = { - /** Source for the avatar. Can be a URL or an icon. */ - source?: AvatarSource; - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. - * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. - */ - fallbackIcon?: AvatarSource; - - /** Denotes whether it is an avatar or a workspace avatar */ - type?: AvatarType; - - /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name?: string; - - /** ID of the Icon */ - id?: string; -}; - type UserDetailsTooltipProps = ChildrenProps & { /** User's Account ID */ accountID: number; @@ -43,7 +24,7 @@ type UserDetailsTooltipProps = ChildrenProps & { fallbackUserDetails?: FallbackUserDetails; /** Optionally, pass in the icon instead of calculating it. If defined, will take precedence. */ - icon?: Icon; + icon?: IconType; /** The accountID of the copilot who took this action on behalf of the user */ delegateAccountID?: number; diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 07fca587ea24..3091d93858c4 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -1,6 +1,5 @@ import React, {memo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; -import type {AvatarProps} from '@components/Avatar'; import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; @@ -39,7 +38,7 @@ type ReportActionItemFragmentProps = { delegateAccountID?: number; /** icon */ - actorIcon?: AvatarProps; + actorIcon?: OnyxCommon.Icon; /** Whether the comment is a thread parent message/the first message in a thread */ isThreadParentMessage?: boolean; diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 8b96a89a2a1b..c4a3afc3e0b9 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -25,10 +25,10 @@ type AvatarType = typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPA type Icon = { /** Avatar source to display */ - source: AvatarSource; + source?: AvatarSource; /** Denotes whether it is an avatar or a workspace avatar */ - type: AvatarType; + type?: AvatarType; /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; From d831b0d9d3ea6d6856a7e2e9a1d0c341ee985985 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Tue, 16 Apr 2024 09:44:57 +0200 Subject: [PATCH 105/580] Minor code improvements --- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 7116b9ab3a77..afc114c8436b 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -264,7 +264,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; // canUseP2PDistanceRequests is true if the iouType is track expense, but we don't want to allow splitting distance with track expense yet - const isAllowedToSplit = (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && ![CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.TRACK_EXPENSE, CONST.IOU.TYPE.INVOICE].includes(iouType); + const isAllowedToSplit = + (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && ![CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.TRACK_EXPENSE, CONST.IOU.TYPE.INVOICE].includes(iouType); const handleConfirmSelection = useCallback( (keyEvent, option) => { @@ -382,6 +383,5 @@ export default memo( _.isEqual(prevProps.participants, nextProps.participants) && prevProps.iouRequestType === nextProps.iouRequestType && prevProps.iouType === nextProps.iouType && - _.isEqual(prevProps.betas, nextProps.betas) && - prevProps.activePolicyID === nextProps.activePolicyID, + _.isEqual(prevProps.betas, nextProps.betas), ); From 2cfca3186a945025d3c44e3a7a0ac58f190389f9 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Tue, 16 Apr 2024 15:56:16 +0700 Subject: [PATCH 106/580] fix lint --- src/components/OptionListContextProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index e63ce2c63faa..405bb989aec9 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -130,7 +130,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp } Object.values(reports ?? {}) - .filter((report) => (report?.participantAccountIDs?.includes(Number(accoutID)) || (ReportUtils.isSelfDM(report) && report?.ownerAccountID === Number(accoutID)))) + .filter((report) => (Boolean(report?.participantAccountIDs?.includes(Number(accoutID))) || (ReportUtils.isSelfDM(report) && report?.ownerAccountID === Number(accoutID)))) .forEach((report) => { if (!report) { return; From f3abe819f807dc010ee1e551921906c80331abbe Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Tue, 16 Apr 2024 16:05:01 +0700 Subject: [PATCH 107/580] run prettier --- src/components/OptionListContextProvider.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 405bb989aec9..94fe51711e02 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -37,12 +37,11 @@ const OptionsListContext = createContext<OptionsListContextProps>({ areOptionsInitialized: false, }); -const isEqualPersonalDetail = (prevPersonalDetail: PersonalDetails | null, personalDetail: PersonalDetails | null) => ( - prevPersonalDetail?.firstName === personalDetail?.firstName && - prevPersonalDetail?.lastName === personalDetail?.lastName && - prevPersonalDetail?.login === personalDetail?.login && - prevPersonalDetail?.displayName === personalDetail?.displayName - ); +const isEqualPersonalDetail = (prevPersonalDetail: PersonalDetails | null, personalDetail: PersonalDetails | null) => + prevPersonalDetail?.firstName === personalDetail?.firstName && + prevPersonalDetail?.lastName === personalDetail?.lastName && + prevPersonalDetail?.login === personalDetail?.login && + prevPersonalDetail?.displayName === personalDetail?.displayName; function OptionsListContextProvider({reports, children}: OptionsListProviderProps) { const areOptionsInitialized = useRef(false); @@ -130,7 +129,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp } Object.values(reports ?? {}) - .filter((report) => (Boolean(report?.participantAccountIDs?.includes(Number(accoutID))) || (ReportUtils.isSelfDM(report) && report?.ownerAccountID === Number(accoutID)))) + .filter((report) => Boolean(report?.participantAccountIDs?.includes(Number(accoutID))) || (ReportUtils.isSelfDM(report) && report?.ownerAccountID === Number(accoutID))) .forEach((report) => { if (!report) { return; From 531b3c523aa18efe2695965b585b69fcbd4fe0e9 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Tue, 16 Apr 2024 11:47:04 +0200 Subject: [PATCH 108/580] Fix canSendInvoice check --- src/libs/PolicyUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 22d7c2e76200..7239bf68a05a 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,6 +1,8 @@ import Str from 'expensify-common/lib/str'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import * as NetworkStore from '@libs/Network/NetworkStore'; +import type {RootStackParamList, State} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -10,7 +12,6 @@ import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getPolicyIDFromState from './Navigation/getPolicyIDFromState'; import Navigation, {navigationRef} from './Navigation/Navigation'; -import type {RootStackParamList, State} from './Navigation/types'; type MemberEmailsToAccountIDs = Record<string, number>; @@ -321,7 +322,7 @@ function getPolicyIDFromNavigationState() { /** Return active policies where current user is an admin */ function getActiveAdminWorkspaces(policies: OnyxCollection<Policy>): Policy[] { const activePolicies = getActivePolicies(policies); - return activePolicies.filter((policy) => isPolicyAdmin(policy)); + return activePolicies.filter((policy) => shouldShowPolicy(policy, NetworkStore.isOffline()) && isPolicyAdmin(policy)); } /** Whether the user can send invoice */ From 65e1251f38a16b7f133bc807c81b700c98465f0d Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Tue, 16 Apr 2024 16:58:16 +0700 Subject: [PATCH 109/580] feature: add props isSplit to button with dropdown --- .../ButtonWithDropdownMenu/index.tsx | 76 ++++++++++++------- .../ButtonWithDropdownMenu/types.ts | 3 + src/pages/workspace/WorkspaceMembersPage.tsx | 1 + .../categories/WorkspaceCategoriesPage.tsx | 1 + .../distanceRates/PolicyDistanceRatesPage.tsx | 1 + .../workspace/tags/WorkspaceTagsPage.tsx | 1 + .../workspace/taxes/WorkspaceTaxesPage.tsx | 1 + 7 files changed, 56 insertions(+), 28 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index a4e6e2c87fec..7ee225ad9ace 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; import Icon from '@components/Icon'; @@ -10,10 +10,12 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; +import type IconAsset from '@src/types/utils/IconAsset'; import type {ButtonWithDropdownMenuProps} from './types'; function ButtonWithDropdownMenu<IValueType>({ success = false, + isSplit = true, isLoading = false, isDisabled = false, pressOnEnter = false, @@ -40,7 +42,7 @@ function ButtonWithDropdownMenu<IValueType>({ const [isMenuVisible, setIsMenuVisible] = useState(false); const [popoverAnchorPosition, setPopoverAnchorPosition] = useState<AnchorPosition | null>(null); const {windowWidth, windowHeight} = useWindowDimensions(); - const caretButton = useRef<View & HTMLDivElement>(null); + const caretButton = useRef<View | HTMLDivElement | null>(null); const selectedItem = options[selectedItemIndex] || options[0]; const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize); const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE; @@ -65,6 +67,18 @@ function ButtonWithDropdownMenu<IValueType>({ } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); + const iconRight = useMemo( + () => ( + <Icon + medium={isButtonSizeLarge} + small={!isButtonSizeLarge} + src={Expensicons.DownArrow} + fill={success ? theme.buttonSuccessText : theme.icon} + /> + ), + [isButtonSizeLarge, success, theme.buttonSuccessText, theme.icon], + ); + return ( <View style={wrapperStyle}> {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( @@ -72,8 +86,10 @@ function ButtonWithDropdownMenu<IValueType>({ <Button success={success} pressOnEnter={pressOnEnter} - ref={buttonRef} - onPress={(event) => onPress(event, selectedItem.value)} + ref={(ref) => { + caretButton.current = ref; + }} + onPress={(event) => (!isSplit ? setIsMenuVisible(!isMenuVisible) : onPress(event, selectedItem.value))} text={customText ?? selectedItem.text} isDisabled={isDisabled || !!selectedItem.disabled} isLoading={isLoading} @@ -81,34 +97,38 @@ function ButtonWithDropdownMenu<IValueType>({ style={[styles.flex1, styles.pr0]} large={isButtonSizeLarge} medium={!isButtonSizeLarge} - innerStyles={[innerStyleDropButton, customText !== undefined && styles.cursorDefault, customText !== undefined && styles.pointerEventsNone]} + innerStyles={[innerStyleDropButton, !isSplit && styles.dropDownButtonCartIconView]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} + iconRight={iconRight as IconAsset} + shouldShowRightIcon={!isSplit} /> - <Button - ref={caretButton} - success={success} - isDisabled={isDisabled} - style={[styles.pl0]} - onPress={() => setIsMenuVisible(!isMenuVisible)} - shouldRemoveLeftBorderRadius - large={isButtonSizeLarge} - medium={!isButtonSizeLarge} - innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton]} - enterKeyEventListenerPriority={enterKeyEventListenerPriority} - > - <View style={[styles.dropDownButtonCartIconView, innerStyleDropButton]}> - <View style={[success ? styles.buttonSuccessDivider : styles.buttonDivider]} /> - <View style={[isButtonSizeLarge ? styles.dropDownLargeButtonArrowContain : styles.dropDownMediumButtonArrowContain]}> - <Icon - medium={isButtonSizeLarge} - small={!isButtonSizeLarge} - src={Expensicons.DownArrow} - fill={success ? theme.buttonSuccessText : theme.icon} - /> + {isSplit && ( + <Button + ref={caretButton} + success={success} + isDisabled={isDisabled} + style={[styles.pl0]} + onPress={() => setIsMenuVisible(!isMenuVisible)} + shouldRemoveLeftBorderRadius + large={isButtonSizeLarge} + medium={!isButtonSizeLarge} + innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton]} + enterKeyEventListenerPriority={enterKeyEventListenerPriority} + > + <View style={[styles.dropDownButtonCartIconView, innerStyleDropButton]}> + <View style={[success ? styles.buttonSuccessDivider : styles.buttonDivider]} /> + <View style={[isButtonSizeLarge ? styles.dropDownLargeButtonArrowContain : styles.dropDownMediumButtonArrowContain]}> + <Icon + medium={isButtonSizeLarge} + small={!isButtonSizeLarge} + src={Expensicons.DownArrow} + fill={success ? theme.buttonSuccessText : theme.icon} + /> + </View> </View> - </View> - </Button> + </Button> + )} </View> ) : ( <Button diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index 87db9a29d827..1b4533087e44 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -74,6 +74,9 @@ type ButtonWithDropdownMenuProps<TValueType> = { /** Additional style to add to the wrapper */ wrapperStyle?: StyleProp<ViewStyle>; + + /** Whether the button should use split style or not */ + isSplit?: boolean; }; export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps, WorkspaceTaxRatesBulkActionType}; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index dfaf50c0bcf6..f7427c865636 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -515,6 +515,7 @@ function WorkspaceMembersPage({policyMembers, personalDetails, invitedEmailsToAc onPress={() => null} options={getBulkActionsButtonOptions()} buttonRef={dropdownButtonRef} + isSplit={false} style={[isSmallScreenWidth && styles.flexGrow1]} /> ) : ( diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 5c2dbe6de352..105cd9d28d67 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -208,6 +208,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} customText={translate('workspace.common.selected', {selectedNumber: selectedCategoriesArray.length})} options={options} + isSplit={false} style={[isSmallScreenWidth && styles.flexGrow1, isSmallScreenWidth && styles.mb3]} /> ); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 37c827ebdf31..5faf99e0c59a 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -253,6 +253,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) buttonRef={dropdownButtonRef} style={[isSmallScreenWidth && styles.flexGrow1]} wrapperStyle={styles.w100} + isSplit={false} /> )} </View> diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index eed0015a9aa4..d01d7c8d3b4d 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -216,6 +216,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { onPress={() => null} shouldAlwaysShowDropdownMenu pressOnEnter + isSplit={false} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} customText={translate('workspace.common.selected', {selectedNumber: selectedTagsArray.length})} options={options} diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 72680760f08f..a956cf9663ae 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -226,6 +226,7 @@ function WorkspaceTaxesPage({ customText={translate('workspace.common.selected', {selectedNumber: selectedTaxesIDs.length})} shouldAlwaysShowDropdownMenu pressOnEnter + isSplit={false} style={[isSmallScreenWidth && styles.w50, isSmallScreenWidth && styles.mb3]} /> ); From 3cc506d9a4d779f2c4c3a0eec12e74b46878d911 Mon Sep 17 00:00:00 2001 From: Tsaqif <tsaiinkwa@yahoo.com> Date: Tue, 16 Apr 2024 17:08:19 +0700 Subject: [PATCH 110/580] revert ROUTES Signed-off-by: Tsaqif <tsaiinkwa@yahoo.com> --- src/ROUTES.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 13f094e3e7fe..8e4c84f24965 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -475,10 +475,6 @@ const ROUTES = { route: 'settings/workspaces/:policyID/invite-message', getRoute: (policyID: string) => `settings/workspaces/${policyID}/invite-message` as const, }, - WORKSPACE_SETTINGS: { - route: 'workspace/:policyID/settings', - getRoute: (policyID: string) => `workspace/${policyID}/settings` as const, - }, WORKSPACE_PROFILE: { route: 'settings/workspaces/:policyID/profile', getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile` as const, @@ -508,8 +504,8 @@ const ROUTES = { getRoute: (policyID: string, inviterEmail: string) => `settings/workspaces/${policyID}/join?email=${inviterEmail}` as const, }, WORKSPACE_SETTINGS_CURRENCY: { - route: 'workspace/:policyID/settings/currency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, + route: 'settings/workspaces/:policyID/settings/currency', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/settings/currency` as const, }, WORKSPACE_WORKFLOWS: { route: 'settings/workspaces/:policyID/workflows', From c19eb591b20772a6ea0aaa27d9383ea97540a598 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Tue, 16 Apr 2024 12:34:16 +0200 Subject: [PATCH 111/580] fix PR comments --- .../settings/Wallet/ActivatePhysicalCardPage.tsx | 5 ++++- src/pages/settings/Wallet/ExpensifyCardPage.tsx | 14 +++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index 86b44494b5fb..5fddc63d6071 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -110,8 +110,11 @@ function ActivatePhysicalCardPage({ setFormError('activateCardPage.error.thatDidntMatch'); return; } + if (physicalCard?.cardID) { + return; + } - CardSettings.activatePhysicalExpensifyCard(lastFourDigits, physicalCard?.cardID ?? 0); + CardSettings.activatePhysicalExpensifyCard(lastFourDigits, physicalCard?.cardID); }, [lastFourDigits, physicalCard?.cardID]); if (isEmptyObject(physicalCard)) { diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 95e2631b0f95..e0b5e45464d4 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -104,30 +104,30 @@ function ExpensifyCardPage({ const [isCardDetailsLoading, setIsCardDetailsLoading] = useState<Record<number, boolean>>({}); const [cardsDetailsErrors, setCardsDetailsErrors] = useState<Record<number, string>>({}); - const handleRevealDetails = (revealedcardID: number) => { + const handleRevealDetails = (revealedCardID: number) => { setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({ ...prevState, - [revealedcardID]: true, + [revealedCardID]: true, })); // We can't store the response in Onyx for security reasons. // That is why this action is handled manually and the response is stored in a local state // Hence eslint disable here. // eslint-disable-next-line rulesdir/no-thenable-actions-in-views - Card.revealVirtualCardDetails(revealedcardID) + Card.revealVirtualCardDetails(revealedCardID) .then((value) => { - setCardsDetails((prevState: Record<number, TCardDetails | null>) => ({...prevState, [revealedcardID]: value as TCardDetails})); + setCardsDetails((prevState: Record<number, TCardDetails | null>) => ({...prevState, [revealedCardID]: value as TCardDetails})); setCardsDetailsErrors((prevState) => ({ ...prevState, - [revealedcardID]: '', + [revealedCardID]: '', })); }) .catch((error) => { setCardsDetailsErrors((prevState) => ({ ...prevState, - [revealedcardID]: error, + [revealedCardID]: error, })); }) - .finally(() => setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({...prevState, [revealedcardID]: false}))); + .finally(() => setIsCardDetailsLoading((prevState: Record<number, boolean>) => ({...prevState, [revealedCardID]: false}))); }; const hasDetectedDomainFraud = cardsToShow?.some((card) => card?.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN); From ae6f3594d09fb9b912c3270273112a358b25e829 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Tue, 16 Apr 2024 12:35:24 +0200 Subject: [PATCH 112/580] fix types --- src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index 5fddc63d6071..6dc3b5d168c4 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -110,7 +110,7 @@ function ActivatePhysicalCardPage({ setFormError('activateCardPage.error.thatDidntMatch'); return; } - if (physicalCard?.cardID) { + if (physicalCard?.cardID === undefined) { return; } From 15554f20e15c2428426545cc63c2f718926c25e2 Mon Sep 17 00:00:00 2001 From: Tsaqif <tsaiinkwa@yahoo.com> Date: Tue, 16 Apr 2024 17:37:43 +0700 Subject: [PATCH 113/580] Change display text format of currency selection list and pretify a file Signed-off-by: Tsaqif <tsaiinkwa@yahoo.com> --- src/components/CurrencySelectionList/index.tsx | 2 +- src/pages/iou/request/step/IOURequestStepCurrency.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 4546ddfc1bad..70edaabeb551 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -17,7 +17,7 @@ function CurrencySelectionList({textInputLabel, initiallySelectedCurrencyCode, o const isSelectedCurrency = currencyCode === initiallySelectedCurrencyCode; return { currencyName: currencyInfo?.name ?? '', - text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, + text: `${currencyCode} - ${CurrencyUtils.getCurrencySymbol(currencyCode)}`, currencyCode, keyForList: currencyCode, isSelected: isSelectedCurrency, diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.tsx b/src/pages/iou/request/step/IOURequestStepCurrency.tsx index efab449b6eac..d3e1efd04fa0 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.tsx +++ b/src/pages/iou/request/step/IOURequestStepCurrency.tsx @@ -5,6 +5,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import CurrencySelectionList from '@components/CurrencySelectionList'; import type {CurrencyListItem} from '@components/CurrencySelectionList/types'; import useLocalize from '@hooks/useLocalize'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; @@ -14,7 +15,6 @@ import ROUTES, {getUrlWithBackToParam} from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Transaction} from '@src/types/onyx'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; import StepScreenWrapper from './StepScreenWrapper'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; From dafdc75220ab410b1069755bfb7a414ad059a6a3 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Tue, 16 Apr 2024 13:36:27 +0200 Subject: [PATCH 114/580] Minor code improvements --- src/components/MenuItem.tsx | 2 +- src/libs/PolicyUtils.ts | 4 ++-- src/libs/ReportUtils.ts | 12 +++++++----- src/libs/actions/Policy.ts | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 98911f5152c8..d6e223c3b024 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -88,7 +88,7 @@ type MenuItemBaseProps = { badgeStyle?: ViewStyle; /** Any additional styles to apply to the label */ - labelStyle?: ViewStyle; + labelStyle?: StyleProp<ViewStyle>; /** Any adjustments to style when menu item is hovered or pressed */ hoverAndPressStyle?: StyleProp<AnimatedStyle<ViewStyle>>; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7239bf68a05a..c3a05e90b322 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,8 +1,6 @@ import Str from 'expensify-common/lib/str'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import * as NetworkStore from '@libs/Network/NetworkStore'; -import type {RootStackParamList, State} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -12,6 +10,8 @@ import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getPolicyIDFromState from './Navigation/getPolicyIDFromState'; import Navigation, {navigationRef} from './Navigation/Navigation'; +import type {RootStackParamList, State} from './Navigation/types'; +import * as NetworkStore from './Network/NetworkStore'; type MemberEmailsToAccountIDs = Record<string, number>; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a796566a82e9..c57ee4bd93df 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -874,7 +874,7 @@ function isPolicyExpenseChat(report: OnyxEntry<Report> | Participant | EmptyObje } /** - * Whether the provided report is an Invoice room chat. + * Whether the provided report is an invoice room chat. */ function isInvoiceRoom(report: OnyxEntry<Report>): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; @@ -3184,10 +3184,12 @@ function getPolicyDescriptionText(policy: OnyxEntry<Policy>): string { return parser.htmlToText(policy.description); } +/** Builds an optimistic reportAction for the invite message */ function buildOptimisticInviteReportAction(invitedUserDisplayName: string, invitedUserID: number): OptimisticInviteReportAction { const text = `${Localize.translateLocal('workspace.invite.invited')} ${invitedUserDisplayName}`; const commentText = getParsedComment(text); const parser = new ExpensiMark(); + const currentUser = allPersonalDetails?.[currentUserAccountID ?? -1]; return { reportActionID: NumberUtils.rand64(), @@ -3196,12 +3198,12 @@ function buildOptimisticInviteReportAction(invitedUserDisplayName: string, invit person: [ { style: 'strong', - text: allPersonalDetails?.[currentUserAccountID ?? -1]?.displayName ?? currentUserEmail, + text: currentUser?.displayName ?? currentUserEmail, type: 'TEXT', }, ], automatic: false, - avatar: allPersonalDetails?.[currentUserAccountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + avatar: currentUser?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created: DateUtils.getDBTime(), message: [ { @@ -3423,7 +3425,7 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe return result.trim().length ? result : formula; } -/** Builds an optimistic Invoice report with a randomly generated reportID */ +/** Builds an optimistic invoice report with a randomly generated reportID */ function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, receiverAccountID: number, receiverName: string, total: number, currency: string): OptimisticExpenseReport { const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency); @@ -3996,7 +3998,7 @@ function buildOptimisticChatReport( }; if (chatType === CONST.REPORT.CHAT_TYPE.INVOICE) { - // TODO: update to support workspace receiver when workspace-workspace invoice room exists + // TODO: update to support workspace as an invoice receiver when workspace-to-workspace invoice room implemented optimisticChatReport.invoiceReceiver = { type: 'individual', accountID: participantList[0], diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 8662a2cf5925..aa6efd0c10e4 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -288,7 +288,7 @@ function getPolicy(policyID: string | undefined): Policy | EmptyObject { } /** - * Returns the primary policy for the user + * Returns a primary policy for the user */ function getPrimaryPolicy(activePolicyID?: string): Policy | undefined { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); From 37879990afebb64342f0b623e246b6a85d9ae1ed Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab <abzokhattab@gmail.com> Date: Tue, 16 Apr 2024 14:28:23 +0200 Subject: [PATCH 115/580] Bumping react-fast-pdf to v1.0.12 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 203e062de680..f0d2c8b19ea3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.6", + "react-fast-pdf": "^1.0.12", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", @@ -30920,9 +30920,9 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.6.tgz", - "integrity": "sha512-CdAnBSZaLCGLSEuiqWLzzXhV9Wvdf1VRixaXCrb3NFrXyeltahF7PY+u7eU6ynrWZGmNI6g0cMLPv0DQhJEeew==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.12.tgz", + "integrity": "sha512-RSIYTwQVKWFqZKtmtzd4JU/FnsqdGPBtHu/N6xl7TsauAFnEouUJNjmC7Rg/pd010OX1UvyraQKdBIZ5Pf2q0A==", "dependencies": { "react-pdf": "^7.7.0", "react-window": "^1.8.10" diff --git a/package.json b/package.json index 43a3ed8cae6a..7513d420b3e5 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.1.0", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.6", + "react-fast-pdf": "^1.0.12", "react-map-gl": "^7.1.3", "react-native": "0.73.4", "react-native-android-location-enabler": "^2.0.1", From 1e0e3affeca5380768746069b0223b3e6107b051 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Tue, 16 Apr 2024 15:06:59 +0200 Subject: [PATCH 116/580] feat: add phone number page --- src/languages/en.ts | 2 + src/languages/es.ts | 2 + .../AddBankAccount/AddBankAccount.tsx | 2 +- .../PersonalInfo/PersonalInfo.tsx | 53 ++++++++--- .../PersonalInfo/substeps/PhoneNumber.tsx | 92 +++++++++++++++++++ .../utils/getInitialSubstepForPersonalInfo.ts | 33 +++++++ src/types/form/PersonalBankAccountForm.ts | 2 + src/types/onyx/WalletAdditionalDetails.ts | 1 + 8 files changed, 173 insertions(+), 14 deletions(-) create mode 100644 src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx create mode 100644 src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts diff --git a/src/languages/en.ts b/src/languages/en.ts index 9451407c822f..91a1714e3c79 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1700,6 +1700,8 @@ export default { address: 'Address', letsDoubleCheck: "Let's double check that everything looks right.", byAddingThisBankAccount: 'By adding this bank account, you confirm that you have read, understand and accept', + whatsYourPhoneNumber: 'What’s your phone number?', + weNeedThisToVerify: 'We need this to verify your wallet.', }, businessInfoStep: { businessInfo: 'Business info', diff --git a/src/languages/es.ts b/src/languages/es.ts index a56c8ac2739d..a50922a8e4d9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1725,6 +1725,8 @@ export default { address: 'Dirección', letsDoubleCheck: 'Revisemos que todo esté bien', byAddingThisBankAccount: 'Añadiendo esta cuenta bancaria, confirmas que has leído, entendido y aceptado', + whatsYourPhoneNumber: '¿Cuál es tu número de teléfono?', + weNeedThisToVerify: 'Necesitamos esto para verificar tu billetera.', }, businessInfoStep: { businessInfo: 'Información de la empresa', diff --git a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx index afa69fe14bc0..bcb0f420c235 100644 --- a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx +++ b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx @@ -54,7 +54,7 @@ function AddBankAccount({personalBankAccount, plaidData, personalBankAccountDraf const exitFlow = (shouldContinue = false) => { const exitReportID = personalBankAccount?.exitReportID; - const onSuccessFallbackRoute = personalBankAccount?.onSuccessFallbackRoute ?? ''; + const onSuccessFallbackRoute = ROUTES.SETTINGS_ENABLE_PAYMENTS_REFACTOR; if (exitReportID) { Navigation.dismissModal(exitReportID); diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index ea97d8cfcaa3..f96c67b4699c 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -11,13 +11,16 @@ import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import getInitialSubstepForPersonalInfo from '@pages/ReimbursementAccount/utils/getInitialSubstepForPersonalInfo'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; +import PhoneNumber from '@pages/EnablePayments/PersonalInfo/substeps/PhoneNumber'; import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import INPUT_IDS, {PersonalBankAccountForm} from '@src/types/form/PersonalBankAccountForm'; +import type {PersonalBankAccountForm} from '@src/types/form/PersonalBankAccountForm'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; import type {UserWallet, WalletAdditionalDetails} from '@src/types/onyx'; +import getInitialSubstepForPersonalInfo from '../utils/getInitialSubstepForPersonalInfo'; import getSubstepValues from '../utils/getSubstepValues'; import Address from './substeps/Address'; import Confirmation from './substeps/Confirmation'; @@ -25,7 +28,7 @@ import DateOfBirth from './substeps/DateOfBirth'; import FullName from './substeps/FullName'; import SocialSecurityNumber from './substeps/SocialSecurityNumber'; -type EnablePaymentsPageOnyxProps = { +type PersonalInfoPageOnyxProps = { /** The user's wallet */ userWallet: OnyxEntry<UserWallet>; @@ -36,21 +39,34 @@ type EnablePaymentsPageOnyxProps = { walletAdditionalDetailsDraft: OnyxEntry<PersonalBankAccountForm>; }; -type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps; +type PersonalInfoPageProps = PersonalInfoPageOnyxProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; -const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, SocialSecurityNumber, Address, Confirmation]; +const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, SocialSecurityNumber, PhoneNumber, Address, Confirmation]; -function EnablePaymentsPage({userWallet, walletAdditionalDetails, walletAdditionalDetailsDraft}: EnablePaymentsPageProps) { +function PersonalInfoPage({userWallet, walletAdditionalDetails, walletAdditionalDetailsDraft}: PersonalInfoPageProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); const {isPendingOnfidoResult, hasFailedOnfido} = userWallet ?? {}; - const submit = () => {}; - const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, walletAdditionalDetailsDraft, walletAdditionalDetails), [walletAdditionalDetails, walletAdditionalDetailsDraft]); + const submit = () => { + const personalDetails = { + phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', + legalFirstName: values?.[PERSONAL_INFO_STEP_KEYS.FIRST_NAME] ?? '', + legalLastName: values?.[PERSONAL_INFO_STEP_KEYS.LAST_NAME] ?? '', + addressStreet: values?.[PERSONAL_INFO_STEP_KEYS.STREET] ?? '', + addressCity: values?.[PERSONAL_INFO_STEP_KEYS.CITY] ?? '', + addressState: values?.[PERSONAL_INFO_STEP_KEYS.STATE] ?? '', + addressZip: values?.[PERSONAL_INFO_STEP_KEYS.ZIP_CODE] ?? '', + dob: values?.[PERSONAL_INFO_STEP_KEYS.DOB] ?? '', + ssn: values?.[PERSONAL_INFO_STEP_KEYS.SSN_LAST_4] ?? '', + }; + // Attempt to set the personal details + Wallet.updatePersonalDetails(personalDetails); + }; useEffect(() => { if (isOffline) { @@ -68,13 +84,23 @@ function EnablePaymentsPage({userWallet, walletAdditionalDetails, walletAddition const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(values), [values]); - const {componentToRender: SubStep, isEditing, nextScreen, prevScreen, moveTo} = useSubStep({bodyContent, startFrom, onFinished: submit}); + const { + componentToRender: SubStep, + isEditing, + nextScreen, + prevScreen, + moveTo, + } = useSubStep({ + bodyContent, + startFrom, + onFinished: submit, + }); return ( <ScreenWrapper shouldShowOfflineIndicator={userWallet?.currentStep !== CONST.WALLET.STEP.ONFIDO} includeSafeAreaPaddingBottom={false} - testID={EnablePaymentsPage.displayName} + testID={PersonalInfoPage.displayName} > <HeaderWithBackButton title={translate('personalInfoStep.personalInfo')} @@ -95,9 +121,9 @@ function EnablePaymentsPage({userWallet, walletAdditionalDetails, walletAddition ); } -EnablePaymentsPage.displayName = 'EnablePaymentsPage'; +PersonalInfoPage.displayName = 'PersonalInfoPage'; -export default withOnyx<EnablePaymentsPageProps, EnablePaymentsPageOnyxProps>({ +export default withOnyx<PersonalInfoPageProps, PersonalInfoPageOnyxProps>({ userWallet: { key: ONYXKEYS.USER_WALLET, @@ -109,7 +135,8 @@ export default withOnyx<EnablePaymentsPageProps, EnablePaymentsPageOnyxProps>({ walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM walletAdditionalDetailsDraft: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, }, -})(EnablePaymentsPage); +})(PersonalInfoPage); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx new file mode 100644 index 000000000000..2ec47be4d7a8 --- /dev/null +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {WalletAdditionalDetails} from '@src/types/onyx'; + +type PhoneNumberOnyxProps = { + /** Reimbursement account from ONYX */ + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; +}; + +type PhoneNumberProps = PhoneNumberOnyxProps & SubStepProps; + +const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; +const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; + +const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => { + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { + errors.ssnLast4 = 'bankAccount.error.ssnLast4'; + } + + return errors; +}; +function PhoneNumber({walletAdditionalDetails, onNext, isEditing}: PhoneNumberProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const defaultPhoneNumber = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.PHONE_NUMBER] ?? ''; + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + <FormProvider + formID={ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS} + submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} + validate={validate} + onSubmit={handleSubmit} + style={[styles.mh5, styles.flexGrow1]} + submitButtonStyles={[styles.pb5, styles.mb0]} + > + <View> + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.whatsYourPhoneNumber')}</Text> + <Text style={[styles.textSupporting]}>{translate('personalInfoStep.weNeedThisToVerify')}</Text> + <View style={[styles.flex1]}> + <InputWrapper + InputComponent={TextInput} + inputID={PERSONAL_INFO_STEP_KEY.PHONE_NUMBER} + label={translate('common.phoneNumber')} + aria-label={translate('common.phoneNumber')} + role={CONST.ROLE.PRESENTATION} + inputMode={CONST.INPUT_MODE.TEL} + placeholder={translate('common.phoneNumberPlaceholder')} + defaultValue={defaultPhoneNumber} + shouldSaveDraft={!isEditing} + containerStyles={[styles.mt6]} + /> + </View> + <HelpLinks containerStyles={[styles.mt5]} /> + </View> + </FormProvider> + ); +} + +PhoneNumber.displayName = 'PhoneNumber'; + +export default withOnyx<PhoneNumberProps, PhoneNumberOnyxProps>({ + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + walletAdditionalDetails: { + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + }, +})(PhoneNumber); diff --git a/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts b/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts new file mode 100644 index 000000000000..82f5567e7f9d --- /dev/null +++ b/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts @@ -0,0 +1,33 @@ +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {PersonalInfoStepProps} from '@src/types/form/PersonalBankAccountForm'; + +const personalInfoKeys = INPUT_IDS.PERSONAL_INFO_STEP; + +/** + * Returns the initial substep for the Personal Info step based on already existing data + */ +function getInitialSubstepForPersonalInfo(data: PersonalInfoStepProps): number { + if (data[personalInfoKeys.FIRST_NAME] === '' || data[personalInfoKeys.LAST_NAME] === '') { + return 0; + } + + if (data[personalInfoKeys.DOB] === '') { + return 1; + } + + if (data[personalInfoKeys.SSN_LAST_4] === '') { + return 2; + } + + if (data[personalInfoKeys.PHONE_NUMBER] === '') { + return 3; + } + + if (data[personalInfoKeys.STREET] === '' || data[personalInfoKeys.CITY] === '' || data[personalInfoKeys.STATE] === '' || data[personalInfoKeys.ZIP_CODE] === '') { + return 4; + } + + return 5; +} + +export default getInitialSubstepForPersonalInfo; diff --git a/src/types/form/PersonalBankAccountForm.ts b/src/types/form/PersonalBankAccountForm.ts index c07c62663567..8d0ff4f3869c 100644 --- a/src/types/form/PersonalBankAccountForm.ts +++ b/src/types/form/PersonalBankAccountForm.ts @@ -22,6 +22,7 @@ const INPUT_IDS = { CITY: 'addressCity', STATE: 'addressState', ZIP_CODE: 'addressZipCode', + PHONE_NUMBER: 'phoneNumber', IS_ONFIDO_SETUP_COMPLETE: 'isOnfidoSetupComplete', }, } as const; @@ -44,6 +45,7 @@ type PersonalInfoStepProps = { [INPUT_IDS.PERSONAL_INFO_STEP.STATE]: string; [INPUT_IDS.PERSONAL_INFO_STEP.ZIP_CODE]: string; [INPUT_IDS.PERSONAL_INFO_STEP.DOB]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.PHONE_NUMBER]: string; [INPUT_IDS.PERSONAL_INFO_STEP.SSN_LAST_4]: string; [INPUT_IDS.PERSONAL_INFO_STEP.IS_ONFIDO_SETUP_COMPLETE]: boolean; }; diff --git a/src/types/onyx/WalletAdditionalDetails.ts b/src/types/onyx/WalletAdditionalDetails.ts index 4ed1cd1f2ed7..f2a9d10b1293 100644 --- a/src/types/onyx/WalletAdditionalDetails.ts +++ b/src/types/onyx/WalletAdditionalDetails.ts @@ -29,6 +29,7 @@ type WalletAdditionalDetails = { addressCity: string; addressState: string; addressZipCode: string; + phoneNumber: string; }; export default WalletAdditionalDetails; From 98fef234cb71846c68b6ac4fde5434d0f7f5f344 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Tue, 16 Apr 2024 21:46:09 +0700 Subject: [PATCH 117/580] use useCallBack to get icon right --- src/components/ButtonWithDropdownMenu/index.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 7ee225ad9ace..9c2ab4b36734 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; import Icon from '@components/Icon'; @@ -10,7 +10,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; -import type IconAsset from '@src/types/utils/IconAsset'; import type {ButtonWithDropdownMenuProps} from './types'; function ButtonWithDropdownMenu<IValueType>({ @@ -67,7 +66,7 @@ function ButtonWithDropdownMenu<IValueType>({ } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); - const iconRight = useMemo( + const getIconRightButton = useCallback( () => ( <Icon medium={isButtonSizeLarge} @@ -99,7 +98,7 @@ function ButtonWithDropdownMenu<IValueType>({ medium={!isButtonSizeLarge} innerStyles={[innerStyleDropButton, !isSplit && styles.dropDownButtonCartIconView]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} - iconRight={iconRight as IconAsset} + iconRight={getIconRightButton} shouldShowRightIcon={!isSplit} /> From ffc26f9788b6b5d95ce7f00818b3957013639a13 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Tue, 16 Apr 2024 16:58:07 +0200 Subject: [PATCH 118/580] feat: add validation for the field, editing phone number --- src/CONST.ts | 5 +++-- .../AddBankAccount/AddBankAccount.tsx | 1 + .../EnablePayments/PersonalInfo/PersonalInfo.tsx | 2 +- .../PersonalInfo/substeps/Confirmation.tsx | 13 +++++++++++-- .../PersonalInfo/substeps/PhoneNumber.tsx | 7 +++---- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 74e722cdba59..93ffadd826c9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1313,8 +1313,9 @@ const CONST = { PERSONAL_INFO: { LEGAL_NAME: 0, DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, + PHONE_NUMBER: 2, + SSN: 3, + ADDRESS: 4, }, }, TIER_NAME: { diff --git a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx index bcb0f420c235..82bdb3e6bdc4 100644 --- a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx +++ b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx @@ -54,6 +54,7 @@ function AddBankAccount({personalBankAccount, plaidData, personalBankAccountDraf const exitFlow = (shouldContinue = false) => { const exitReportID = personalBankAccount?.exitReportID; + // TODO: This should be updated to the correct route once the refactor is complete const onSuccessFallbackRoute = ROUTES.SETTINGS_ENABLE_PAYMENTS_REFACTOR; if (exitReportID) { diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index f96c67b4699c..92b005ab8a88 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -42,7 +42,7 @@ type PersonalInfoPageOnyxProps = { type PersonalInfoPageProps = PersonalInfoPageOnyxProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; -const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, SocialSecurityNumber, PhoneNumber, Address, Confirmation]; +const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, PhoneNumber, SocialSecurityNumber, Address, Confirmation]; function PersonalInfoPage({userWallet, walletAdditionalDetails, walletAdditionalDetailsDraft}: PersonalInfoPageProps) { const {translate} = useLocalize(); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx index 866cc30611a2..49caca27db9f 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx @@ -14,11 +14,11 @@ import useNetwork from '@hooks/useNetwork'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; -import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; import type {WalletAdditionalDetails} from '@src/types/onyx'; +import getSubstepValues from '../../utils/getSubstepValues'; type ConfirmationOnyxProps = { /** wallet additional details from ONYX */ @@ -31,7 +31,7 @@ type ConfirmationOnyxProps = { type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; -const PERSONAL_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX.PERSONAL_INFO; +const PERSONAL_INFO_STEP_INDEXES = CONST.WALLET.SUBSTEP_INDEXES.PERSONAL_INFO; function Confirmation({walletAdditionalDetails, walletAdditionalDetailsDraft, onNext, onMove}: ConfirmationProps) { const {translate} = useLocalize(); @@ -66,6 +66,14 @@ function Confirmation({walletAdditionalDetails, walletAdditionalDetailsDraft, on onMove(PERSONAL_INFO_STEP_INDEXES.DATE_OF_BIRTH); }} /> + <MenuItemWithTopDescription + description={translate('common.phoneNumber')} + title={values[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER]} + shouldShowRightIcon + onPress={() => { + onMove(PERSONAL_INFO_STEP_INDEXES.PHONE_NUMBER); + }} + /> <MenuItemWithTopDescription description={translate('personalInfoStep.last4SSN')} title={values[PERSONAL_INFO_STEP_KEYS.SSN_LAST_4]} @@ -139,6 +147,7 @@ export default withOnyx<ConfirmationProps, ConfirmationOnyxProps>({ walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM walletAdditionalDetailsDraft: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, }, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx index 2ec47be4d7a8..b465efe6fe79 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx @@ -26,15 +26,14 @@ type PhoneNumberOnyxProps = { type PhoneNumberProps = PhoneNumberOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; -const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; +const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.PHONE_NUMBER]; const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { - errors.ssnLast4 = 'bankAccount.error.ssnLast4'; + if (values.phoneNumber && !ValidationUtils.isValidUSPhone(values.phoneNumber, true)) { + errors.phoneNumber = 'bankAccount.error.phoneNumber'; } - return errors; }; function PhoneNumber({walletAdditionalDetails, onNext, isEditing}: PhoneNumberProps) { From 96aa755bc2af5775ce9ff93757777ad6fa192a58 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Tue, 16 Apr 2024 16:59:04 +0200 Subject: [PATCH 119/580] Put iouCreatedAction to optimistic data --- src/libs/actions/IOU.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 72245e27ff6d..d4224595b775 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -882,6 +882,7 @@ function buildOnyxDataForInvoice( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, value: { + [iouCreatedAction.reportActionID]: iouCreatedAction as OnyxTypes.ReportAction, [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, }, }, From 93292f7ef05da5c5d2b1ae404b378dca72fbc89a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 16:43:50 +0200 Subject: [PATCH 120/580] add invoice chat type --- src/CONST.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CONST.ts b/src/CONST.ts index 74e722cdba59..7f26b9ecaf36 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -52,6 +52,7 @@ const chatTypes = { POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', SELF_DM: 'selfDM', + INVOICE: 'invoiceRoom', } as const; // Explicit type annotation is required From 1c17dd6eae4add9972ed335118a926ff453e4ade Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 16:44:30 +0200 Subject: [PATCH 121/580] add invoice report type --- src/CONST.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CONST.ts b/src/CONST.ts index 7f26b9ecaf36..c30f94db6b83 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -791,6 +791,7 @@ const CONST = { EXPENSE: 'expense', IOU: 'iou', TASK: 'task', + INVOICE: 'invoice', }, CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { From 08c640b4e670e62bbb478459bda79d1e2af20f8d Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 16:45:40 +0200 Subject: [PATCH 122/580] create isInvoiceRoom and isInvoiceReport --- src/libs/ReportUtils.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f7b160bd67e2..9f1ebb40c0c2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5898,6 +5898,20 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyID: s return isChatRoom(report) && !isThread(report); } +/** + * Check if Report is an invoice room + */ +function isInvoiceRoom(report: OnyxEntry<Report>): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; +} + +/** + * Check if Report is an invoice report + */ +function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { + return report?.type === CONST.REPORT.TYPE.INVOICE; +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -6134,6 +6148,8 @@ export { buildParticipantsFromAccountIDs, canReportBeMentionedWithinPolicy, getAllHeldTransactions, + isInvoiceRoom, + isInvoiceReport, }; export type { From 10c2d2fb9f4427b06995771abe6c52397cdcc0ee Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 16:47:31 +0200 Subject: [PATCH 123/580] check isInvoiceReport in report screen --- src/pages/home/ReportScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 332e9b080558..7e31b69779c2 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -343,7 +343,7 @@ function ReportScreen({ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(route.params.reportID)); }, [transactionThreadReportID, route.params.reportActionID, route.params.reportID]); - if (ReportUtils.isMoneyRequestReport(report)) { + if (ReportUtils.isMoneyRequestReport(report) || ReportUtils.isInvoiceReport(report)) { headerView = ( <MoneyReportHeader report={report} From 5022a11b055719c155a7cb5a04099376208e94bf Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 16:49:26 +0200 Subject: [PATCH 124/580] check that invoice room is chat room --- src/libs/ReportUtils.ts | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9f1ebb40c0c2..b92c7d4b9b50 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -905,11 +905,25 @@ function isPaidGroupPolicyExpenseReport(report: OnyxEntry<Report>): boolean { return isExpenseReport(report) && isPaidGroupPolicy(report); } +/** + * Check if Report is an invoice room + */ +function isInvoiceRoom(report: OnyxEntry<Report>): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; +} + +/** + * Check if Report is an invoice report + */ +function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { + return report?.type === CONST.REPORT.TYPE.INVOICE; +} + /** * Whether the provided report is a chat room */ function isChatRoom(report: OnyxEntry<Report>): boolean { - return isUserCreatedPolicyRoom(report) || isDefaultRoom(report); + return isUserCreatedPolicyRoom(report) || isDefaultRoom(report) || isInvoiceRoom(report); } /** @@ -5898,20 +5912,6 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyID: s return isChatRoom(report) && !isThread(report); } -/** - * Check if Report is an invoice room - */ -function isInvoiceRoom(report: OnyxEntry<Report>): boolean { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; -} - -/** - * Check if Report is an invoice report - */ -function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { - return report?.type === CONST.REPORT.TYPE.INVOICE; -} - export { getReportParticipantsTitle, isReportMessageAttachment, @@ -5929,6 +5929,8 @@ export { isAdminsOnlyPostingRoom, isAnnounceRoom, isUserCreatedPolicyRoom, + isInvoiceRoom, + isInvoiceReport, isChatRoom, getChatRoomSubtitle, getParentNavigationSubtitle, @@ -6148,8 +6150,6 @@ export { buildParticipantsFromAccountIDs, canReportBeMentionedWithinPolicy, getAllHeldTransactions, - isInvoiceRoom, - isInvoiceReport, }; export type { From 86f52ca2cea996d56e5acfcaf66fac4413f5af95 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 16:55:17 +0200 Subject: [PATCH 125/580] implement invoice room welcome message --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ReportUtils.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 9451407c822f..20779fa561a6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -519,6 +519,7 @@ export default { // eslint-disable-next-line @typescript-eslint/naming-convention 'track-expense': 'track an expense', }, + beginningOfChatHistoryInvoiceRoom: 'Collaboration starts here! 🎉 \nUse this room to view, discuss, and pay invoices.', }, reportAction: { asCopilot: 'as copilot for', diff --git a/src/languages/es.ts b/src/languages/es.ts index a56c8ac2739d..0d55f4a902dd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -515,6 +515,7 @@ export default { // eslint-disable-next-line @typescript-eslint/naming-convention 'track-expense': 'rastrear un gasto', }, + beginningOfChatHistoryInvoiceRoom: '¡Este es el lugar para colaborar! 🎉\nUsa esta sala para ver, discutir y pagar facturas.', }, reportAction: { asCopilot: 'como copiloto de', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b92c7d4b9b50..123610acbf34 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1497,6 +1497,8 @@ function getRoomWelcomeMessage(report: OnyxEntry<Report>, isUserPolicyAdmin: boo } else if (isAnnounceRoom(report)) { welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName}); + } else if (isInvoiceRoom(report)) { + welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom'); } else { // Message for user created rooms or other room types. welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartOne'); From f9243bbde5a65a264b46620fb83cf8ef86806574 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 17:00:11 +0200 Subject: [PATCH 126/580] accept invoice report in AvatarWithDisplayName --- src/components/AvatarWithDisplayName.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index f6afb4dae2d6..4171a3170534 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -60,7 +60,8 @@ function AvatarWithDisplayName({ const title = ReportUtils.getReportName(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); - const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report); + const isMoneyRequestOrReport = + ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); From c15f33d6442772f84ae03394f018049e06ac6ae8 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 17:05:08 +0200 Subject: [PATCH 127/580] support invoice report in getChatRoomSubtitle --- src/libs/ReportUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 123610acbf34..1669a619d14d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3062,6 +3062,9 @@ function getChatRoomSubtitle(report: OnyxEntry<Report>): string | undefined { if (isArchivedRoom(report)) { return report?.oldPolicyName ?? ''; } + if (isInvoiceRoom(report)) { + return Localize.translateLocal('workspace.common.invoices'); + } return getPolicyName(report); } From e08889ed8dd3c3e7c75a1abb8496d8d40468b020 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 17:07:40 +0200 Subject: [PATCH 128/580] accept invoice room in shouldReportShowSubscript --- src/libs/ReportUtils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1669a619d14d..2f054cc60788 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5112,6 +5112,10 @@ function shouldReportShowSubscript(report: OnyxEntry<Report>): boolean { return true; } + if (isInvoiceRoom(report)) { + return true; + } + return false; } From 2d67b99043a1194c72959c7736fc69874f91e75e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 17:54:33 +0200 Subject: [PATCH 129/580] integrate invoice room in canLeaveRoom --- src/libs/ReportUtils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2f054cc60788..0b30e1d9676e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5041,8 +5041,15 @@ function getMoneyRequestOptions(report: OnyxEntry<Report>, policy: OnyxEntry<Pol * `domain` - Nobody can leave (it's auto-shared with domain members) * `dm` - Nobody can leave (it's auto-shared with users) * `private` - Anybody can leave (though you can only be invited to join) + * `invoice` - Invoice sender, invoice receiver and auto-invited admins cannot leave */ function canLeaveRoom(report: OnyxEntry<Report>, isPolicyMember: boolean): boolean { + if (report?.chatType === CONST.REPORT.CHAT_TYPE.INVOICE) { + const isAdmin = !!Object.entries(report.participants ?? {}).find(([participantID, {role}]) => Number(participantID) === currentUserAccountID && role !== CONST.POLICY.ROLE.ADMIN); + + return report.managerID !== currentUserAccountID && report.ownerAccountID !== currentUserAccountID && !isAdmin; + } + if (!report?.visibility) { if ( report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS || From 3e946169042201531aee923417eff56e7bf9b5af Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 10 Apr 2024 18:17:48 +0200 Subject: [PATCH 130/580] integrate invoice icon in getIcons --- src/libs/ReportUtils.ts | 17 +++++++++++++++++ src/types/onyx/Report.ts | 2 ++ 2 files changed, 19 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0b30e1d9676e..97c6f59ed26f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1955,6 +1955,23 @@ function getIcons( return [groupChatIcon]; } + if (isInvoiceRoom(report)) { + const workspaceIcon = getWorkspaceIcon(report, policy); + + const receiverPolicyID = Object.values(report.participants ?? {})?.find((participant) => participant.type === 'policy')?.policyID ?? ''; + const receiverPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${receiverPolicyID}`]; + const isWorkspaceToWorkspace = !!receiverPolicyID && receiverPolicy; + const icons = []; + + if (isWorkspaceToWorkspace) { + icons.push(getWorkspaceIcon(report, receiverPolicy)); + } else { + icons.push(...getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails)); + } + + return [workspaceIcon, ...icons]; + } + return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails); } diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index fe3eec6dc11e..cf39d1f0d1bd 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -26,6 +26,8 @@ type PendingChatMember = { type Participant = { hidden?: boolean; role?: 'admin' | 'member'; + type?: 'policy' | 'individual'; + policyID?: string; }; type Participants = Record<number, Participant>; From 8a41a366cd6b76515b1b2cd6809b9e4fe69ccaad Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 11 Apr 2024 17:56:20 +0200 Subject: [PATCH 131/580] create getInvoicesChatName --- src/libs/ReportUtils.ts | 23 +++++++++++++++++++++++ src/types/onyx/Report.ts | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 97c6f59ed26f..937d54d77702 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2974,6 +2974,24 @@ function getReportActionMessage(reportAction: ReportAction | EmptyObject, parent return reportAction?.message?.[0]?.text ?? ''; } +/** + * Get the title for an invoice room. + */ +function getInvoicesChatName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>): string { + const policyName = getPolicyName(report, false, policy); + let receiverName = ''; + + if (report?.invoiceReceiverAccountID) { + receiverName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[report.invoiceReceiverAccountID]); + } + + if (report?.invoiceReceiverPolicyID) { + receiverName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiverPolicyID}`]); + } + + return `${receiverName} & ${policyName}`; +} + /** * Get the title for a report. */ @@ -3045,6 +3063,10 @@ function getReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = nu formattedName = getDisplayNameForParticipant(currentUserAccountID, undefined, undefined, true); } + if (isInvoiceRoom(report)) { + formattedName = getInvoicesChatName(report, policy); + } + if (formattedName) { return formattedName; } @@ -5999,6 +6021,7 @@ export { getIcons, getRoomWelcomeMessage, getDisplayNamesWithTooltips, + getInvoicesChatName, getReportName, getReport, getReportNotificationPreference, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index cf39d1f0d1bd..9aba3f2c2a34 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -188,6 +188,10 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< transactionThreadReportID?: string; fieldList?: Record<string, PolicyReportField>; + + // TODO: Confirm + invoiceReceiverAccountID?: number; + invoiceReceiverPolicyID?: string; }, PolicyReportField['fieldID'] >; From 24460570f31f6f00717315b306b26fbfd0e0412c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 11:11:25 +0200 Subject: [PATCH 132/580] add awaiting payment translation --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/languages/types.ts | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 20779fa561a6..df87737eb504 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9,6 +9,7 @@ import type { AlreadySignedInParams, AmountEachParams, ApprovedAmountParams, + AwaitingPaymentParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, @@ -736,6 +737,7 @@ export default { set: 'set', changed: 'changed', removed: 'removed', + awaitingPayment: ({payerName}: AwaitingPaymentParams) => `Awaiting payment by ${payerName}`, }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0d55f4a902dd..280852813fb5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7,6 +7,7 @@ import type { AlreadySignedInParams, AmountEachParams, ApprovedAmountParams, + AwaitingPaymentParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, @@ -734,6 +735,7 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', + awaitingPayment: ({payerName}: AwaitingPaymentParams) => `A la espera de pago por ${payerName}`, }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/languages/types.ts b/src/languages/types.ts index c365363f84af..59e1bfe40af2 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -135,6 +135,8 @@ type PayerSettledParams = {amount: number | string}; type WaitingOnBankAccountParams = {submitterDisplayName: string}; +type AwaitingPaymentParams = {payerName: string}; + type CanceledRequestParams = {amount: string; submitterDisplayName: string}; type AdminCanceledRequestParams = {manager: string; amount: string}; @@ -391,6 +393,7 @@ export type { ViolationsTagOutOfPolicyParams, ViolationsTaxOutOfPolicyParams, WaitingOnBankAccountParams, + AwaitingPaymentParams, WalletProgramParams, UsePlusButtonParams, WeSentYouMagicSignInLinkParams, From 8e667005d581a6bb9cccb7d6f398f754f6b97ade Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 11:15:32 +0200 Subject: [PATCH 133/580] create PaymentWaitingBanner --- src/components/PaymentWaitingBanner/index.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/components/PaymentWaitingBanner/index.tsx diff --git a/src/components/PaymentWaitingBanner/index.tsx b/src/components/PaymentWaitingBanner/index.tsx new file mode 100644 index 000000000000..af13711f08e0 --- /dev/null +++ b/src/components/PaymentWaitingBanner/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type PaymentWaitingBannerProps = { + payerName: string; +}; + +function PaymentWaitingBanner({payerName}: PaymentWaitingBannerProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + <View style={[styles.flexRow, styles.alignItemsCenter]}> + <Icon + src={Expensicons.Hourglass} + fill={theme.icon} + /> + + <Text style={[styles.inlineSystemMessage, styles.flexShrink1]}>{translate('iou.awaitingPayment', {payerName})}</Text> + </View> + ); +} + +export default PaymentWaitingBanner; From fd8a5bd6bfc9076a1be48454c84fb289ed58837f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 11:17:31 +0200 Subject: [PATCH 134/580] create isInvoiceAwaitingPayment --- src/libs/ReportUtils.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 937d54d77702..f92c0ad7b5ef 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5967,6 +5967,13 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyID: s return isChatRoom(report) && !isThread(report); } +/** + * Check if a invoice report is awaiting for payment + */ +function isInvoiceAwaitingPayment(report: OnyxEntry<Report>): boolean { + return !isSettled(report?.reportID ?? '') && isInvoiceReport(report); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -6206,6 +6213,7 @@ export { buildParticipantsFromAccountIDs, canReportBeMentionedWithinPolicy, getAllHeldTransactions, + isInvoiceAwaitingPayment, }; export type { From 19175e973c0084c56fcc305fb91b15b7f95904e6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 11:19:10 +0200 Subject: [PATCH 135/580] render MoneyRequestView in ReportActionItem for invoice --- src/pages/home/report/ReportActionItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index b2922556b71d..06dc68bf8fae 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -752,7 +752,7 @@ function ReportActionItem({ </View> ); } - if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report)) { + if (ReportUtils.isExpenseReport(report) || ReportUtils.isIOUReport(report) || ReportUtils.isInvoiceReport(report)) { return ( <OfflineWithFeedback pendingAction={action.pendingAction}> {transactionThreadReport && !isEmptyObject(transactionThreadReport) ? ( From 21fb170d9bd79da63f39c7e902d9c219d31c30c3 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 11:59:31 +0200 Subject: [PATCH 136/580] add invoiceReceiver to report --- src/CONST.ts | 5 +++++ src/types/onyx/Report.ts | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index c30f94db6b83..9f4fd2c4128b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4339,6 +4339,11 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, + + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + POLICY: 'policy', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 9aba3f2c2a34..56980094cd71 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -189,9 +189,15 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< fieldList?: Record<string, PolicyReportField>; - // TODO: Confirm - invoiceReceiverAccountID?: number; - invoiceReceiverPolicyID?: string; + invoiceReceiver?: + | { + type: typeof CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + accountID: number; + } + | { + type: typeof CONST.INVOICE_RECEIVER_TYPE.POLICY; + policyID: string; + }; }, PolicyReportField['fieldID'] >; From 32536ffaee59a86a1312869d084b40c075e524bb Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 12:04:07 +0200 Subject: [PATCH 137/580] integrate invoiceReceiver usage --- src/libs/ReportUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f92c0ad7b5ef..5321ae30d075 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2981,12 +2981,12 @@ function getInvoicesChatName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy const policyName = getPolicyName(report, false, policy); let receiverName = ''; - if (report?.invoiceReceiverAccountID) { - receiverName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[report.invoiceReceiverAccountID]); + if (report?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { + receiverName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[report.invoiceReceiver.accountID]); } - if (report?.invoiceReceiverPolicyID) { - receiverName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiverPolicyID}`]); + if (report?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.POLICY) { + receiverName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`]); } return `${receiverName} & ${policyName}`; From 4bad9a2fa580f3ba1291ab57e6c5ee04556aa013 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 12:06:00 +0200 Subject: [PATCH 138/580] add invoice banner to MoneyReportHeader --- src/components/MoneyReportHeader.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 14227d6a2051..befcaf470901 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -22,6 +22,7 @@ import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; +import PaymentWaitingBanner from './PaymentWaitingBanner'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import SettlementButton from './SettlementButton'; @@ -99,6 +100,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldShowWaitingNote = ReportUtils.isInvoiceAwaitingPayment(moneyRequestReport); + const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; @@ -108,7 +111,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy); const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount; - const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && isSmallScreenWidth); + const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && isSmallScreenWidth) || shouldShowWaitingNote; const confirmPayment = (type?: PaymentMethodType | undefined) => { if (!type) { @@ -263,6 +266,12 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money <MoneyReportHeaderStatusBar nextStep={nextStep} /> </View> )} + {shouldShowWaitingNote && ( + <PaymentWaitingBanner + // TODO: Integrate a getter for the payer name + payerName="" + /> + )} </View> {isHoldMenuVisible && requestType !== undefined && ( <ProcessMoneyReportHoldMenu From 4e81a6c86a7a00ffbaa7692f17cf82d29f0cfb24 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 12:11:07 +0200 Subject: [PATCH 139/580] integrate deleted invoice message --- src/components/ReportActionItem/MoneyRequestAction.tsx | 2 ++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/pages/home/report/ReportActionItem.tsx | 2 ++ 4 files changed, 6 insertions(+) diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index 7d9ba2697c7a..9bef637bf292 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -116,6 +116,8 @@ function MoneyRequestAction({ message = 'parentReportAction.reversedTransaction'; } else if (isTrackExpenseAction) { message = 'parentReportAction.deletedExpense'; + } else if (action.childType === CONST.REPORT.TYPE.INVOICE) { + message = 'parentReportAction.deletedInvoice'; } else { message = 'parentReportAction.deletedRequest'; } diff --git a/src/languages/en.ts b/src/languages/en.ts index df87737eb504..b38601420c2d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2535,6 +2535,7 @@ export default { reversedTransaction: '[Reversed transaction]', deletedTask: '[Deleted task]', hiddenMessage: '[Hidden message]', + deletedInvoice: '[Deleted invoice]', }, threads: { thread: 'Thread', diff --git a/src/languages/es.ts b/src/languages/es.ts index 280852813fb5..2263170425c3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3027,6 +3027,7 @@ export default { reversedTransaction: '[Transacción anulada]', deletedTask: '[Tarea eliminada]', hiddenMessage: '[Mensaje oculto]', + deletedInvoice: '[Factura eliminada]', }, threads: { thread: 'Hilo', diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 06dc68bf8fae..afc66d15c1f1 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -689,6 +689,8 @@ function ReportActionItem({ message = 'parentReportAction.reversedTransaction'; } else if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) { message = 'parentReportAction.deletedExpense'; + } else if (parentReportAction?.childType === CONST.REPORT.TYPE.INVOICE) { + message = 'parentReportAction.deletedInvoice'; } else { message = 'parentReportAction.deletedRequest'; } From 19b54e34b25b9d9ddcb7c27ae4b237cdcac744ff Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 12:12:58 +0200 Subject: [PATCH 140/580] integrate deleted invoice message in getTransactionReportName --- src/libs/ReportUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5321ae30d075..7aa2d4a7278b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -190,6 +190,7 @@ type OptimisticIOUReportAction = Pick< | 'receipt' | 'whisperedToAccountIDs' | 'childReportID' + | 'childType' >; type ReportRouteParams = { @@ -2676,6 +2677,10 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis return Localize.translateLocal('iou.receiptMissingDetails'); } + if (reportAction?.childType === CONST.REPORT.TYPE.INVOICE) { + return Localize.translateLocal('parentReportAction.deletedInvoice'); + } + const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { From ca98bd9383b22f0a90cbe6c854cb46c81355350f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 12:16:09 +0200 Subject: [PATCH 141/580] add invoice banner to ReportPreview --- src/components/ReportActionItem/ReportPreview.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 190343e48abd..9ff09ad33ec9 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -8,6 +8,7 @@ import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import PaymentWaitingBanner from '@components/PaymentWaitingBanner'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RenderHTML from '@components/RenderHTML'; import SettlementButton from '@components/SettlementButton'; @@ -215,6 +216,8 @@ function ReportPreview({ const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID); const shouldShowRBR = !iouSettled && hasErrors; + const shouldShowWaitingNote = ReportUtils.isInvoiceAwaitingPayment(iouReport); + /* Show subtitle if at least one of the money requests is not being smart scanned, and either: - There is more than one money request – in this case, the "X requests, Y scanning" subtitle is shown; @@ -352,6 +355,12 @@ function ReportPreview({ isDisabled={shouldDisableSubmitButton} /> )} + {shouldShowWaitingNote && ( + <PaymentWaitingBanner + // TODO: Integrate a getter for the payer name + payerName="" + /> + )} </View> </View> </View> From 89e2eedf79ca35828eb6216d1a12cced528b1155 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 12 Apr 2024 12:17:42 +0200 Subject: [PATCH 142/580] add todo --- src/types/onyx/Report.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 56980094cd71..2bad31bc372c 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -26,6 +26,7 @@ type PendingChatMember = { type Participant = { hidden?: boolean; role?: 'admin' | 'member'; + // TODO: Confirm type?: 'policy' | 'individual'; policyID?: string; }; From d83d22aee6806b16db5513a5b2089cc6f2fb335f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 15 Apr 2024 14:41:11 +0200 Subject: [PATCH 143/580] invoice is not a default for ReportWelcomeText --- src/components/ReportWelcomeText.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 219199c25bc3..24d7747a8fc7 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -38,7 +38,8 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isChatRoom = ReportUtils.isChatRoom(report); const isSelfDM = ReportUtils.isSelfDM(report); - const isDefault = !(isChatRoom || isPolicyExpenseChat || isSelfDM); + const isInvoiceRoom = ReportUtils.isInvoiceRoom(report); + const isDefault = !(isChatRoom || isPolicyExpenseChat || isSelfDM || isInvoiceRoom); const participantAccountIDs = report?.participantAccountIDs ?? []; const isMultipleParticipant = participantAccountIDs.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); From b9ed19d390825fe254c4f36e64d799e85f017e9d Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 15 Apr 2024 14:41:21 +0200 Subject: [PATCH 144/580] fix welcome message --- src/libs/ReportUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7aa2d4a7278b..8305e90d20d3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1499,6 +1499,7 @@ function getRoomWelcomeMessage(report: OnyxEntry<Report>, isUserPolicyAdmin: boo welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName}); welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName}); } else if (isInvoiceRoom(report)) { + welcomeMessage.showReportName = false; welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom'); } else { // Message for user created rooms or other room types. From 697de5e4abb4d115b1fbb7897de2cde886df52f6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 15 Apr 2024 14:44:58 +0200 Subject: [PATCH 145/580] render proper welcome hero text --- src/components/ReportWelcomeText.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 24d7747a8fc7..77c40354ea80 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -59,6 +59,10 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP }; const welcomeHeroText = useMemo(() => { + if (isInvoiceRoom) { + return translate('reportActionsView.sayHello'); + } + if (isChatRoom) { return translate('reportActionsView.welcomeToRoom', {roomName: reportName}); } @@ -68,7 +72,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP } return translate('reportActionsView.sayHello'); - }, [isChatRoom, isSelfDM, translate, reportName]); + }, [isChatRoom, isInvoiceRoom, isSelfDM, translate, reportName]); return ( <> From 60462fd0c3af94813877902a069446633bb3f318 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 15 Apr 2024 15:35:31 +0200 Subject: [PATCH 146/580] render proper invoice room title --- src/libs/ReportUtils.ts | 29 ++++++++++++++++++++--------- src/pages/home/ReportScreen.tsx | 2 ++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8305e90d20d3..c8163a5e2947 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2983,19 +2983,30 @@ function getReportActionMessage(reportAction: ReportAction | EmptyObject, parent /** * Get the title for an invoice room. */ -function getInvoicesChatName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>): string { - const policyName = getPolicyName(report, false, policy); - let receiverName = ''; +function getInvoicesChatName(report: OnyxEntry<Report>): string { + const invoiceReceiver = report?.invoiceReceiver; + const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; + const policyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; + let isReceiver = false; + + if (isIndividual && invoiceReceiverAccountID === currentUserAccountID) { + isReceiver = true; + } + + if (!isIndividual && PolicyUtils.isPolicyMember(policyID, allPolicies)) { + isReceiver = true; + } - if (report?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { - receiverName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[report.invoiceReceiver.accountID]); + if (isReceiver) { + return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]); } - if (report?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.POLICY) { - receiverName = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`]); + if (isIndividual) { + return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]); } - return `${receiverName} & ${policyName}`; + return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]); } /** @@ -3070,7 +3081,7 @@ function getReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = nu } if (isInvoiceRoom(report)) { - formattedName = getInvoicesChatName(report, policy); + formattedName = getInvoicesChatName(report); } if (formattedName) { diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 7e31b69779c2..0f32c6a8b5dc 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -204,6 +204,7 @@ function ReportScreen({ isOptimisticReport: reportProp?.isOptimisticReport, lastMentionedTime: reportProp?.lastMentionedTime, avatarUrl: reportProp?.avatarUrl, + invoiceReceiver: reportProp?.invoiceReceiver, }), [ reportProp?.lastReadTime, @@ -243,6 +244,7 @@ function ReportScreen({ reportProp?.isOptimisticReport, reportProp?.lastMentionedTime, reportProp?.avatarUrl, + reportProp?.invoiceReceiver, ], ); From a524346f9402a98b9b6900a167c9ca15a1569078 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 15 Apr 2024 15:53:33 +0200 Subject: [PATCH 147/580] integrate report subtitle for invoice room --- src/languages/en.ts | 4 ++++ src/languages/es.ts | 4 ++++ src/languages/types.ts | 6 ++++++ src/libs/ReportUtils.ts | 39 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b38601420c2d..275d578b8813 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -30,6 +30,8 @@ import type { GoBackMessageParams, GoToRoomParams, InstantSummaryParams, + InvoicesFromParams, + InvoicesToParams, LocalTimeParams, LoggedInAsParams, LogSizeParams, @@ -2144,6 +2146,8 @@ export default { unlockVBACopy: "You're all set to accept payments by ACH or credit card!", viewUnpaidInvoices: 'View unpaid invoices', sendInvoice: 'Send invoice', + invoicesFrom: ({sender}: InvoicesFromParams) => `Invoices from ${sender}`, + invoicesTo: ({receiver}: InvoicesToParams) => `Invoices to ${receiver}`, }, travel: { unlockConciergeBookingTravel: 'Unlock Concierge travel booking', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2263170425c3..0ed33dd533a6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -29,6 +29,8 @@ import type { GoBackMessageParams, GoToRoomParams, InstantSummaryParams, + InvoicesFromParams, + InvoicesToParams, LocalTimeParams, LoggedInAsParams, LogSizeParams, @@ -2172,6 +2174,8 @@ export default { unlockVBACopy: '¡Todo listo para recibir pagos por transferencia o con tarjeta!', viewUnpaidInvoices: 'Ver facturas emitidas pendientes', sendInvoice: 'Enviar factura', + invoicesFrom: ({sender}: InvoicesFromParams) => `Facturas de ${sender}`, + invoicesTo: ({receiver}: InvoicesToParams) => `Facturas a ${receiver}`, }, travel: { unlockConciergeBookingTravel: 'Desbloquea la reserva de viajes con Concierge', diff --git a/src/languages/types.ts b/src/languages/types.ts index 59e1bfe40af2..8c4d287b7dfc 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -251,6 +251,10 @@ type ViolationsTaxOutOfPolicyParams = {taxName?: string}; type TaskCreatedActionParams = {title: string}; +type InvoicesFromParams = {sender: string}; + +type InvoicesToParams = {receiver: string}; + /* Translation Object types */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type TranslationBaseValue = string | string[] | ((...args: any[]) => string); @@ -403,4 +407,6 @@ export type { ZipCodeExampleFormatParams, LogSizeParams, HeldRequestParams, + InvoicesFromParams, + InvoicesToParams, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index c8163a5e2947..db20ba2a45c3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3009,6 +3009,39 @@ function getInvoicesChatName(report: OnyxEntry<Report>): string { return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]); } +/** + * Get the subtitle for an invoice room. + */ +function getInvoicesChatSubtitle(report: OnyxEntry<Report>): string { + const invoiceReceiver = report?.invoiceReceiver; + const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; + const policyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; + let isReceiver = false; + + if (isIndividual && invoiceReceiverAccountID === currentUserAccountID) { + isReceiver = true; + } + + if (!isIndividual && PolicyUtils.isPolicyMember(policyID, allPolicies)) { + isReceiver = true; + } + + if (isReceiver) { + let receiver = ''; + + if (isIndividual) { + receiver = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]); + } else { + receiver = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiver?.policyID}`]); + } + + return Localize.translateLocal('workspace.invoices.invoicesTo', {receiver}); + } + + return Localize.translateLocal('workspace.invoices.invoicesFrom', {sender: getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`])}); +} + /** * Get the title for a report. */ @@ -3102,6 +3135,9 @@ function getReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = nu * Get either the policyName or domainName the chat is tied to */ function getChatRoomSubtitle(report: OnyxEntry<Report>): string | undefined { + if (isInvoiceRoom(report)) { + return getInvoicesChatSubtitle(report); + } if (isChatThread(report)) { return ''; } @@ -3118,9 +3154,6 @@ function getChatRoomSubtitle(report: OnyxEntry<Report>): string | undefined { if (isArchivedRoom(report)) { return report?.oldPolicyName ?? ''; } - if (isInvoiceRoom(report)) { - return Localize.translateLocal('workspace.common.invoices'); - } return getPolicyName(report); } From 933499e25c29b7708abe7008f6d014e24a7b5545 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 15 Apr 2024 16:05:15 +0200 Subject: [PATCH 148/580] integrate payer name --- src/components/MoneyReportHeader.tsx | 9 +++------ src/components/ReportActionItem/ReportPreview.tsx | 9 +++------ src/libs/ReportUtils.ts | 13 +++++++++++++ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index befcaf470901..4f66d3f959b4 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -102,6 +102,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldShowWaitingNote = ReportUtils.isInvoiceAwaitingPayment(moneyRequestReport); + const invoicePayerName = ReportUtils.getInvoicePayerName(chatReport); + const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; @@ -266,12 +268,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money <MoneyReportHeaderStatusBar nextStep={nextStep} /> </View> )} - {shouldShowWaitingNote && ( - <PaymentWaitingBanner - // TODO: Integrate a getter for the payer name - payerName="" - /> - )} + {shouldShowWaitingNote && <PaymentWaitingBanner payerName={invoicePayerName} />} </View> {isHoldMenuVisible && requestType !== undefined && ( <ProcessMoneyReportHoldMenu diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 9ff09ad33ec9..dad91b570311 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -218,6 +218,8 @@ function ReportPreview({ const shouldShowWaitingNote = ReportUtils.isInvoiceAwaitingPayment(iouReport); + const invoicePayerName = ReportUtils.getInvoicePayerName(chatReport); + /* Show subtitle if at least one of the money requests is not being smart scanned, and either: - There is more than one money request – in this case, the "X requests, Y scanning" subtitle is shown; @@ -355,12 +357,7 @@ function ReportPreview({ isDisabled={shouldDisableSubmitButton} /> )} - {shouldShowWaitingNote && ( - <PaymentWaitingBanner - // TODO: Integrate a getter for the payer name - payerName="" - /> - )} + {shouldShowWaitingNote && <PaymentWaitingBanner payerName={invoicePayerName} />} </View> </View> </View> diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index db20ba2a45c3..d4d85be0d933 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2964,6 +2964,17 @@ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Reco return roomName ? `${verb} ${users} ${preposition} ${roomName}` : `${verb} ${users}`; } +function getInvoicePayerName(report: OnyxEntry<Report>): string { + const invoiceReceiver = report?.invoiceReceiver; + const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + + if (isIndividual) { + return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiver.accountID]); + } + + return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiver?.policyID}`]); +} + /** * Get the report action message for a report action. */ @@ -6078,7 +6089,9 @@ export { getIcons, getRoomWelcomeMessage, getDisplayNamesWithTooltips, + getInvoicePayerName, getInvoicesChatName, + getInvoicesChatSubtitle, getReportName, getReport, getReportNotificationPreference, From 80dbeef598dae218b7d43d238701478c8f61651b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 15 Apr 2024 16:28:01 +0200 Subject: [PATCH 149/580] render proper actor of invoice --- src/pages/home/report/ReportActionItemSingle.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 1e0dc432b3fc..7d0b22b22f52 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -84,8 +84,11 @@ function ReportActionItemSingle({ const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport, [action?.actionName, iouReport]); - const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); + const displayAllActors = useMemo( + () => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport && !ReportUtils.isInvoiceReport(iouReport), + [action?.actionName, iouReport], + ); + const isWorkspaceActor = ReportUtils.isInvoiceReport(iouReport ?? {}) || (ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors)); let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); if (isWorkspaceActor) { From 2f5a0be8a68191c54b94c1c9abbbfda789f67304 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Tue, 16 Apr 2024 16:55:12 +0200 Subject: [PATCH 150/580] Support one transaction view for invoices --- src/components/MoneyReportHeader.tsx | 8 ++++++-- src/libs/ReportActionsUtils.ts | 2 +- src/libs/ReportUtils.ts | 15 +++++++++++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 4f66d3f959b4..dbc73973c274 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -193,7 +193,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack(undefined, false, true)} // Shows border if no buttons or next steps are showing below the header - shouldShowBorderBottom={!(shouldShowAnyButton && isSmallScreenWidth) && !(shouldShowNextStep && !isSmallScreenWidth)} + shouldShowBorderBottom={!(shouldShowAnyButton && isSmallScreenWidth) && !(shouldShowNextStep && !isSmallScreenWidth) && !shouldShowWaitingNote} shouldShowThreeDotsButton threeDotsMenuItems={threeDotsMenuItems} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} @@ -268,7 +268,11 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money <MoneyReportHeaderStatusBar nextStep={nextStep} /> </View> )} - {shouldShowWaitingNote && <PaymentWaitingBanner payerName={invoicePayerName} />} + {shouldShowWaitingNote && ( + <View style={[styles.ph5, styles.pb3]}> + <PaymentWaitingBanner payerName={invoicePayerName} /> + </View> + )} </View> {isHoldMenuVisible && requestType !== undefined && ( <ProcessMoneyReportHoldMenu diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index e8f2189f5f7d..21aa31f39bb8 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -228,7 +228,7 @@ function isTransactionThread(parentReportAction: OnyxEntry<ReportAction> | Empty function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry<ReportActions> | ReportAction[]): string | null { // If the report is not an IOU or Expense report, it shouldn't be treated as one-transaction report. const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE) { + if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) { return null; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d4d85be0d933..630f6f70c093 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -920,6 +920,13 @@ function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { return report?.type === CONST.REPORT.TYPE.INVOICE; } +/** + * Checks if the supplied report is an invoice report in Open state and status. + */ +function isOpenInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { + return isInvoiceReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; +} + /** * Whether the provided report is a chat room */ @@ -2414,7 +2421,7 @@ function getMoneyRequestReportName(report: OnyxEntry<Report>, policy: OnyxEntry< return Localize.translateLocal('iou.payerSpentAmount', {payer: payerOrApproverName, amount: formattedAmount}); } - if (isProcessingReport(report) || isOpenExpenseReport(report) || moneyRequestTotal === 0) { + if (isProcessingReport(report) || isOpenExpenseReport(report) || isOpenInvoiceReport(report) || moneyRequestTotal === 0) { return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}); } @@ -3112,7 +3119,7 @@ function getReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = nu formattedName = getPolicyExpenseChatName(report, policy); } - if (isMoneyRequestReport(report)) { + if (isMoneyRequestReport(report) || isInvoiceReport(report)) { formattedName = getMoneyRequestReportName(report, policy); } @@ -3185,6 +3192,10 @@ function getParentNavigationSubtitle(report: OnyxEntry<Report>): ParentNavigatio return {}; } + if (isInvoiceReport(report)) { + return {reportName: `${getPolicyName(parentReport)} & ${getInvoicePayerName(parentReport)}`}; + } + return { reportName: getReportName(parentReport), workspaceName: getPolicyName(parentReport, true), From 875d321e48b2cb0e3518ca5891ec704107f2f27f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Tue, 16 Apr 2024 17:34:09 +0200 Subject: [PATCH 151/580] restrict room leaving --- src/libs/ReportUtils.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 630f6f70c093..ec34ae404606 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5155,10 +5155,31 @@ function getMoneyRequestOptions(report: OnyxEntry<Report>, policy: OnyxEntry<Pol * `invoice` - Invoice sender, invoice receiver and auto-invited admins cannot leave */ function canLeaveRoom(report: OnyxEntry<Report>, isPolicyMember: boolean): boolean { - if (report?.chatType === CONST.REPORT.CHAT_TYPE.INVOICE) { - const isAdmin = !!Object.entries(report.participants ?? {}).find(([participantID, {role}]) => Number(participantID) === currentUserAccountID && role !== CONST.POLICY.ROLE.ADMIN); + if (isInvoiceRoom(report)) { + const invoiceReport = getReport(report?.iouReportID ?? ''); + + if (invoiceReport?.ownerAccountID === currentUserAccountID) { + return false; + } + + if (invoiceReport?.managerID === currentUserAccountID) { + return false; + } + + const isSenderPolicyAdmin = getPolicy(report?.policyID)?.role === CONST.POLICY.ROLE.ADMIN; - return report.managerID !== currentUserAccountID && report.ownerAccountID !== currentUserAccountID && !isAdmin; + if (isSenderPolicyAdmin) { + return false; + } + + const isReceiverPolicyAdmin = + report?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.POLICY ? getPolicy(report?.invoiceReceiver?.policyID)?.role === CONST.POLICY.ROLE.ADMIN : false; + + if (isReceiverPolicyAdmin) { + return false; + } + + return true; } if (!report?.visibility) { From babdb9acb0882a7e82959539d8b14dd1042af360 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Tue, 16 Apr 2024 18:05:46 +0200 Subject: [PATCH 152/580] configure ReportDetailsPage for invoices --- src/libs/ReportUtils.ts | 28 ++++++++++++++++------------ src/pages/ReportDetailsPage.tsx | 24 +++++++++++++----------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ec34ae404606..77c177bcee9a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1964,21 +1964,23 @@ function getIcons( return [groupChatIcon]; } - if (isInvoiceRoom(report)) { - const workspaceIcon = getWorkspaceIcon(report, policy); + if (isInvoiceReport(report)) { + const invoiceRoomReport = getReport(report.chatReportID); + const icons = [getWorkspaceIcon(invoiceRoomReport, policy)]; - const receiverPolicyID = Object.values(report.participants ?? {})?.find((participant) => participant.type === 'policy')?.policyID ?? ''; - const receiverPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${receiverPolicyID}`]; - const isWorkspaceToWorkspace = !!receiverPolicyID && receiverPolicy; - const icons = []; + if (invoiceRoomReport?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { + icons.push(...getIconsForParticipants([invoiceRoomReport?.invoiceReceiver.accountID], personalDetails)); - if (isWorkspaceToWorkspace) { - icons.push(getWorkspaceIcon(report, receiverPolicy)); - } else { - icons.push(...getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails)); + return icons; } - return [workspaceIcon, ...icons]; + const receiverPolicy = getPolicy(invoiceRoomReport?.invoiceReceiver?.policyID); + + if (!isEmptyObject(receiverPolicy)) { + icons.push(getWorkspaceIcon(invoiceRoomReport, receiverPolicy)); + } + + return icons; } return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails); @@ -5642,7 +5644,9 @@ function isReportParticipant(accountID: number, report: OnyxEntry<Report>): bool } function shouldUseFullTitleToDisplay(report: OnyxEntry<Report>): boolean { - return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report) || isGroupChat(report); + return ( + isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report) || isGroupChat(report) || isInvoiceReport(report) + ); } function getRoom(type: ValueOf<typeof CONST.REPORT.CHAT_TYPE>, policyID: string): OnyxEntry<Report> | undefined { diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 7f1dadab8c0e..048b1215787f 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -75,6 +75,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const isChatThread = useMemo(() => ReportUtils.isChatThread(report), [report]); const isArchivedRoom = useMemo(() => ReportUtils.isArchivedRoom(report), [report]); const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(report), [report]); + const isInvoiceReport = useMemo(() => ReportUtils.isInvoiceReport(report), [report]); const canEditReportDescription = useMemo(() => ReportUtils.canEditReportDescription(report, policy), [report, policy]); const shouldShowReportDescription = isChatRoom && (canEditReportDescription || report.description !== ''); @@ -182,7 +183,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD }); // Prevent displaying private notes option for threads and task reports - if (!isChatThread && !isMoneyRequestReport && !ReportUtils.isTaskReport(report)) { + if (!isChatThread && !isMoneyRequestReport && !isInvoiceReport && !ReportUtils.isTaskReport(report)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.PRIVATE_NOTES, translationKey: 'privateNotes.title', @@ -195,19 +196,20 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD return items; }, [ + isSelfDM, + isGroupDMChat, isArchivedRoom, - participants.length, + isGroupChat, + isDefaultRoom, isChatThread, - isMoneyRequestReport, - report, - isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, - session, - isSelfDM, - isDefaultRoom, + participants.length, + report, + isMoneyRequestReport, + isInvoiceReport, activeChatMembers.length, - isGroupChat, + session, ]); const displayNamesWithTooltips = useMemo(() => { @@ -261,7 +263,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD <ScrollView style={[styles.flex1]}> <View style={styles.reportDetailsTitleContainer}> <View style={styles.mb3}> - {isMoneyRequestReport ? ( + {isMoneyRequestReport || isInvoiceReport ? ( <MultipleAvatars icons={icons} size={CONST.AVATAR_SIZE.LARGE} @@ -297,7 +299,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD ) : ( chatRoomSubtitleText )} - {!isEmptyObject(parentNavigationSubtitleData) && isMoneyRequestReport && ( + {!isEmptyObject(parentNavigationSubtitleData) && (isMoneyRequestReport || isInvoiceReport) && ( <ParentNavigationSubtitle parentNavigationSubtitleData={parentNavigationSubtitleData} parentReportID={report?.parentReportID} From 62303174cfc4738766247b36640082a7e7c767b6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Tue, 16 Apr 2024 19:00:21 +0200 Subject: [PATCH 153/580] check if the user can pay an invoice --- src/libs/actions/IOU.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 85f4b74f3436..e98fac17bee8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4857,6 +4857,14 @@ function canIOUBePaid(iouReport: OnyxEntry<OnyxTypes.Report> | EmptyObject, chat return false; } + if (ReportUtils.isInvoiceReport(iouReport)) { + if (chatReport?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { + return chatReport?.invoiceReceiver?.accountID === userAccountID; + } + + return getPolicy(chatReport?.invoiceReceiver?.policyID).role === CONST.POLICY.ROLE.ADMIN; + } + const isPayer = ReportUtils.isPayer( { email: currentUserEmail, From dc229aed780d51b24c74b276f4000606886b180b Mon Sep 17 00:00:00 2001 From: neil-marcellini <neil@expensify.com> Date: Tue, 16 Apr 2024 10:47:30 -0700 Subject: [PATCH 154/580] Define the TransactionCustomUnit type --- src/types/onyx/Transaction.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 281b6b4228ce..12820ec43c06 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -47,12 +47,20 @@ type Comment = { waypoints?: WaypointCollection; isLoading?: boolean; type?: string; - customUnit?: Record<string, unknown>; + customUnit?: TransactionCustomUnit; source?: string; originalTransactionID?: string; splits?: Split[]; }; +type TransactionCustomUnit = { + customUnitID?: string; + customUnitRateID?: string; + quantity?: number; + name?: string; + defaultP2PRate?: number; +}; + type GeometryType = 'LineString'; type Geometry = { From 792cec802b0eba3af7cfa84b2baf0931ddccc39f Mon Sep 17 00:00:00 2001 From: neil-marcellini <neil@expensify.com> Date: Tue, 16 Apr 2024 10:49:19 -0700 Subject: [PATCH 155/580] Update how distance is accessed --- src/components/MoneyRequestConfirmationList.tsx | 2 +- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index aa6c75edbf5d..906a593f254a 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -237,7 +237,7 @@ function MoneyRequestConfirmationList({ rate: 0, currency: CONST.CURRENCY.USD, }; - const distance = transaction?.routes?.route0.distance ?? 0; + const distance = transaction?.comment?.customUnit?.quantity ?? 0; const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; const taxRates = policy?.taxRates; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 21815f00253b..b48e4b31eb0d 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -222,7 +222,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ rate: 0, currency: 'USD', }; - const distance = transaction?.routes?.route0.distance ?? 0; + const distance = transaction?.comment?.customUnit?.quantity ?? 0; const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; const taxRates = policy?.taxRates; From efbbda3de71002b7a1f30e1e0f2bb3ec7c7a6b90 Mon Sep 17 00:00:00 2001 From: GandalfGwaihir <whogandalf@gmail.com> Date: Wed, 17 Apr 2024 05:27:35 +0530 Subject: [PATCH 156/580] Allow the requestee in 1:1 transactions to hold requests --- src/components/MoneyRequestHeader.tsx | 2 +- src/pages/iou/HoldReasonPage.tsx | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index f451f5f15581..1ce9fa31385e 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -97,7 +97,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, IOU.unholdRequest(iouTransactionID, report?.reportID); } else { const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); - Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? '', iouTransactionID, report?.reportID, activeRoute)); + Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, iouTransactionID, report?.reportID, activeRoute)); } }; diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 2a5cba810759..52387fd3a377 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -46,6 +46,11 @@ function HoldReasonPage({route}: HoldReasonPageProps) { const {transactionID, reportID, backTo} = route.params; const report = ReportUtils.getReport(reportID); + + // We check if the report is part of a policy, if not then it's a personal request (1:1 request) + // We need to allow both users in the 1:1 request to put the request on hold + const isWorkspaceRequest = ReportUtils.isGroupPolicy(report); + console.log('isWorkspaceRequest', isWorkspaceRequest); const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); const navigateBack = () => { @@ -53,7 +58,10 @@ function HoldReasonPage({route}: HoldReasonPageProps) { }; const onSubmit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM>) => { - if (!ReportUtils.canEditMoneyRequest(parentReportAction)) { + // We have extra !!isWorkspaceRequest condition as in case of 1:1 request, canEditMoneyRequest will rightly return false + // as we do not allow requestee to edit fields like description and amount, + // but we still want the requestee to be able to put the request on hold + if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { return; } @@ -68,7 +76,11 @@ function HoldReasonPage({route}: HoldReasonPageProps) { if (!values.comment) { errors.comment = 'common.error.fieldRequired'; } - if (!ReportUtils.canEditMoneyRequest(parentReportAction)) { + + // We have extra !!isWorkspaceRequest condition as in case of 1:1 request, canEditMoneyRequest will rightly return false + // as we do not allow requestee to edit fields like description and amount, + // but we still want the requestee to be able to put the request on hold + if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { const formErrors = {}; ErrorUtils.addErrorMessage(formErrors, 'reportModified', 'common.error.requestModified'); FormActions.setErrors(ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM, formErrors); From 9711d32ac5e541d96feb99bb1b66d9c550deb36c Mon Sep 17 00:00:00 2001 From: GandalfGwaihir <whogandalf@gmail.com> Date: Wed, 17 Apr 2024 05:35:47 +0530 Subject: [PATCH 157/580] Remove console log --- src/pages/iou/HoldReasonPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 52387fd3a377..2740fcb0a79a 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -50,7 +50,6 @@ function HoldReasonPage({route}: HoldReasonPageProps) { // We check if the report is part of a policy, if not then it's a personal request (1:1 request) // We need to allow both users in the 1:1 request to put the request on hold const isWorkspaceRequest = ReportUtils.isGroupPolicy(report); - console.log('isWorkspaceRequest', isWorkspaceRequest); const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); const navigateBack = () => { @@ -58,7 +57,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) { }; const onSubmit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM>) => { - // We have extra !!isWorkspaceRequest condition as in case of 1:1 request, canEditMoneyRequest will rightly return false + // We have extra isWorkspaceRequest condition as in case of 1:1 request, canEditMoneyRequest will rightly return false // as we do not allow requestee to edit fields like description and amount, // but we still want the requestee to be able to put the request on hold if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { @@ -76,8 +75,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) { if (!values.comment) { errors.comment = 'common.error.fieldRequired'; } - - // We have extra !!isWorkspaceRequest condition as in case of 1:1 request, canEditMoneyRequest will rightly return false + // We have extra isWorkspaceRequest condition as in case of 1:1 request, canEditMoneyRequest will rightly return false // as we do not allow requestee to edit fields like description and amount, // but we still want the requestee to be able to put the request on hold if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { From 56c484e5432ebdb77dd13da8ce928f85be3929dc Mon Sep 17 00:00:00 2001 From: GandalfGwaihir <whogandalf@gmail.com> Date: Wed, 17 Apr 2024 05:48:20 +0530 Subject: [PATCH 158/580] add isWorkspaceRequest as dependency --- src/pages/iou/HoldReasonPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 2740fcb0a79a..6e31f6202383 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -86,7 +86,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) { return errors; }, - [parentReportAction], + [parentReportAction, isWorkspaceRequest], ); useEffect(() => { From 4f357087bf2739a9e441f108db723b44aad7c297 Mon Sep 17 00:00:00 2001 From: rory <rory@expensify.com> Date: Tue, 16 Apr 2024 17:26:39 -0700 Subject: [PATCH 159/580] Remove unnecessary platform sharding in FloatingMessageCounterContainer --- .../index.tsx => FloatingMessageCounter.tsx} | 7 +++---- .../index.android.tsx | 18 ---------------- .../FloatingMessageCounterContainer/index.tsx | 21 ------------------- .../FloatingMessageCounterContainer/types.ts | 12 ----------- 4 files changed, 3 insertions(+), 55 deletions(-) rename src/pages/home/report/{FloatingMessageCounter/index.tsx => FloatingMessageCounter.tsx} (92%) delete mode 100644 src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx delete mode 100644 src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx delete mode 100644 src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts diff --git a/src/pages/home/report/FloatingMessageCounter/index.tsx b/src/pages/home/report/FloatingMessageCounter.tsx similarity index 92% rename from src/pages/home/report/FloatingMessageCounter/index.tsx rename to src/pages/home/report/FloatingMessageCounter.tsx index d3048848936d..997198192bb6 100644 --- a/src/pages/home/report/FloatingMessageCounter/index.tsx +++ b/src/pages/home/report/FloatingMessageCounter.tsx @@ -9,7 +9,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; -import FloatingMessageCounterContainer from './FloatingMessageCounterContainer'; type FloatingMessageCounterProps = { /** Whether the New Messages indicator is active */ @@ -51,9 +50,9 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating }, [isActive, show, hide]); return ( - <FloatingMessageCounterContainer + <Animated.View accessibilityHint={translate('accessibilityHints.scrollToNewestMessages')} - containerStyles={styles.floatingMessageCounterTransformation(translateY)} + style={[styles.floatingMessageCounterWrapper, styles.floatingMessageCounterTransformation(translateY)]} > <View style={styles.floatingMessageCounter}> <View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}> @@ -79,7 +78,7 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating </Button> </View> </View> - </FloatingMessageCounterContainer> + </Animated.View> ); } diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx deleted file mode 100644 index 64391909b197..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.android.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import {Animated, View} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type FloatingMessageCounterContainerProps from './types'; - -function FloatingMessageCounterContainer({containerStyles, children}: FloatingMessageCounterContainerProps) { - const styles = useThemeStyles(); - - return ( - <Animated.View style={[styles.floatingMessageCounterWrapperAndroid, containerStyles]}> - <View style={styles.floatingMessageCounterSubWrapperAndroid}>{children}</View> - </Animated.View> - ); -} - -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx deleted file mode 100644 index 8757d66160c4..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type FloatingMessageCounterContainerProps from './types'; - -function FloatingMessageCounterContainer({accessibilityHint, containerStyles, children}: FloatingMessageCounterContainerProps) { - const styles = useThemeStyles(); - - return ( - <Animated.View - accessibilityHint={accessibilityHint} - style={[styles.floatingMessageCounterWrapper, containerStyles]} - > - {children} - </Animated.View> - ); -} - -FloatingMessageCounterContainer.displayName = 'FloatingMessageCounterContainer'; - -export default FloatingMessageCounterContainer; diff --git a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts b/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts deleted file mode 100644 index cfe791eed79c..000000000000 --- a/src/pages/home/report/FloatingMessageCounter/FloatingMessageCounterContainer/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type {StyleProp, ViewStyle} from 'react-native'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -type FloatingMessageCounterContainerProps = ChildrenProps & { - /** Styles to be assigned to Container */ - containerStyles?: StyleProp<ViewStyle>; - - /** Specifies the accessibility hint for the component */ - accessibilityHint?: string; -}; - -export default FloatingMessageCounterContainerProps; From 35ea3650ee17950c264c2af6275fec819511e24b Mon Sep 17 00:00:00 2001 From: rory <rory@expensify.com> Date: Tue, 16 Apr 2024 17:28:44 -0700 Subject: [PATCH 160/580] Remove unused styles --- src/styles/index.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 537038d9f2e1..9bba82ac6b95 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3341,21 +3341,6 @@ const styles = (theme: ThemeColors) => ...visibility.hidden, }, - floatingMessageCounterWrapperAndroid: { - left: 0, - width: '100%', - alignItems: 'center', - position: 'absolute', - top: 0, - zIndex: 100, - ...visibility.hidden, - }, - - floatingMessageCounterSubWrapperAndroid: { - left: '50%', - width: 'auto', - }, - floatingMessageCounter: { left: '-50%', ...visibility.visible, From fda608b3b896d4d2ea2ede7101b5cd42ad85c93f Mon Sep 17 00:00:00 2001 From: rory <rory@expensify.com> Date: Tue, 16 Apr 2024 17:31:41 -0700 Subject: [PATCH 161/580] Combine styles together --- src/pages/home/report/FloatingMessageCounter.tsx | 2 +- src/styles/index.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/FloatingMessageCounter.tsx b/src/pages/home/report/FloatingMessageCounter.tsx index 997198192bb6..34eb01b62817 100644 --- a/src/pages/home/report/FloatingMessageCounter.tsx +++ b/src/pages/home/report/FloatingMessageCounter.tsx @@ -52,7 +52,7 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating return ( <Animated.View accessibilityHint={translate('accessibilityHints.scrollToNewestMessages')} - style={[styles.floatingMessageCounterWrapper, styles.floatingMessageCounterTransformation(translateY)]} + style={styles.floatingMessageCounterWrapper(translateY)} > <View style={styles.floatingMessageCounter}> <View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}> diff --git a/src/styles/index.ts b/src/styles/index.ts index 9bba82ac6b95..df94258b5c77 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3333,24 +3333,20 @@ const styles = (theme: ThemeColors) => height: variables.communicationsLinkHeight, }, - floatingMessageCounterWrapper: { + floatingMessageCounterWrapper: (translateY: AnimatableNumericValue) => ({ position: 'absolute', left: '50%', top: 0, zIndex: 100, + transform: [{translateY}], ...visibility.hidden, - }, + }), floatingMessageCounter: { left: '-50%', ...visibility.visible, }, - floatingMessageCounterTransformation: (translateY: AnimatableNumericValue) => - ({ - transform: [{translateY}], - } satisfies ViewStyle), - confirmationAnimation: { height: 180, width: 180, From cf6d23e8bd6eefe3af55e1b6c529e336d69e1ada Mon Sep 17 00:00:00 2001 From: rory <rory@expensify.com> Date: Tue, 16 Apr 2024 17:48:03 -0700 Subject: [PATCH 162/580] Migrate animation to reanimated --- .../home/report/FloatingMessageCounter.tsx | 27 ++++++++++--------- src/styles/index.ts | 5 ++-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/pages/home/report/FloatingMessageCounter.tsx b/src/pages/home/report/FloatingMessageCounter.tsx index 34eb01b62817..8e73d36a878a 100644 --- a/src/pages/home/report/FloatingMessageCounter.tsx +++ b/src/pages/home/report/FloatingMessageCounter.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useEffect, useMemo} from 'react'; -import {Animated, View} from 'react-native'; +import {View} from 'react-native'; +import Animated, {useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -7,7 +8,6 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; type FloatingMessageCounterProps = { @@ -25,20 +25,18 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const translateY = useMemo(() => new Animated.Value(MARKER_INACTIVE_TRANSLATE_Y), []); + const translateY = useSharedValue(MARKER_INACTIVE_TRANSLATE_Y); const show = useCallback(() => { - Animated.spring(translateY, { - toValue: MARKER_ACTIVE_TRANSLATE_Y, - useNativeDriver, - }).start(); + 'worklet'; + + translateY.value = withSpring(MARKER_ACTIVE_TRANSLATE_Y); }, [translateY]); const hide = useCallback(() => { - Animated.spring(translateY, { - toValue: MARKER_INACTIVE_TRANSLATE_Y, - useNativeDriver, - }).start(); + 'worklet'; + + translateY.value = withSpring(MARKER_INACTIVE_TRANSLATE_Y); }, [translateY]); useEffect(() => { @@ -49,10 +47,15 @@ function FloatingMessageCounter({isActive = false, onClick = () => {}}: Floating } }, [isActive, show, hide]); + const wrapperStyle = useAnimatedStyle(() => ({ + ...styles.floatingMessageCounterWrapper, + transform: [{translateY: translateY.value}], + })); + return ( <Animated.View accessibilityHint={translate('accessibilityHints.scrollToNewestMessages')} - style={styles.floatingMessageCounterWrapper(translateY)} + style={wrapperStyle} > <View style={styles.floatingMessageCounter}> <View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}> diff --git a/src/styles/index.ts b/src/styles/index.ts index df94258b5c77..0badb7412b1a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3333,14 +3333,13 @@ const styles = (theme: ThemeColors) => height: variables.communicationsLinkHeight, }, - floatingMessageCounterWrapper: (translateY: AnimatableNumericValue) => ({ + floatingMessageCounterWrapper: { position: 'absolute', left: '50%', top: 0, zIndex: 100, - transform: [{translateY}], ...visibility.hidden, - }), + }, floatingMessageCounter: { left: '-50%', From 1044bad394543fde51d2e89b6f935444b5fd3779 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Wed, 17 Apr 2024 11:49:42 +0700 Subject: [PATCH 163/580] rename props Avatar --- src/components/Avatar.tsx | 6 +++--- src/components/HeaderWithBackButton/index.tsx | 2 +- src/components/MentionSuggestions.tsx | 2 +- src/components/MultipleAvatars.tsx | 8 ++++---- src/components/RoomHeaderAvatars.tsx | 4 ++-- src/components/SubscriptAvatar.tsx | 4 ++-- .../UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx | 2 +- src/pages/home/report/ReportActionItemSingle.tsx | 2 +- src/pages/workspace/WorkspaceProfilePage.tsx | 2 +- src/pages/workspace/WorkspacesListRow.tsx | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index dfcf2cc675b9..09d22132b44e 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -51,7 +51,7 @@ type AvatarProps = { name?: string; /** ID of the Icon */ - iconID?: number | string; + accountID?: number | string; }; function Avatar({ @@ -65,7 +65,7 @@ function Avatar({ fallbackIconTestID = '', type = CONST.ICON_TYPE_AVATAR, name = '', - iconID, + accountID, }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -92,7 +92,7 @@ function Avatar({ let iconColors; if (isWorkspace) { - iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(iconID?.toString() ?? ''); + iconColors = StyleUtils.getDefaultWorkspaceAvatarColor(accountID?.toString() ?? ''); } else if (useFallBackAvatar) { iconColors = StyleUtils.getBackgroundColorAndFill(theme.border, theme.icon); } else { diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 6e649955880a..29d010d9c803 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -182,7 +182,7 @@ function HeaderWithBackButton({ containerStyles={[StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(CONST.AVATAR_SIZE.DEFAULT)), styles.mr3]} source={policyAvatar?.source} name={policyAvatar?.name} - iconID={policyAvatar?.id} + accountID={policyAvatar?.id} type={policyAvatar?.type} /> )} diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index e69484eacadd..b11ae4f5ecd8 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -83,7 +83,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe source={item.icons[0].source} size={isIcon ? CONST.AVATAR_SIZE.MENTION_ICON : CONST.AVATAR_SIZE.SMALLER} name={item.icons[0].name} - iconID={item.icons[0].id} + accountID={item.icons[0].id} type={item.icons[0].type} fill={isIcon ? theme.success : undefined} fallbackIcon={item.icons[0].fallbackIcon} diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index cbceace6df16..d5fcf6607179 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -156,7 +156,7 @@ function MultipleAvatars({ size={size} fill={icons[0].fill} name={icons[0].name} - iconID={icons[0].id} + accountID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} /> @@ -206,7 +206,7 @@ function MultipleAvatars({ source={icon.source ?? fallbackIcon} size={size} name={icon.name} - iconID={icon.id} + accountID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} /> @@ -265,7 +265,7 @@ function MultipleAvatars({ imageStyles={[singleAvatarStyle]} name={icons[0].name} type={icons[0].type} - iconID={icons[0].id} + accountID={icons[0].id} fallbackIcon={icons[0].fallbackIcon} /> </View> @@ -285,7 +285,7 @@ function MultipleAvatars({ size={avatarSize} imageStyles={[singleAvatarStyle]} name={icons[1].name} - iconID={icons[1].id} + accountID={icons[1].id} type={icons[1].type} fallbackIcon={icons[1].fallbackIcon} /> diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index 341398e44b06..bdb4a0ac78ab 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -47,7 +47,7 @@ function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { imageStyles={styles.avatarLarge} size={CONST.AVATAR_SIZE.LARGE} name={icons[0].name} - iconID={icons[0].id} + accountID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} /> @@ -83,7 +83,7 @@ function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { size={CONST.AVATAR_SIZE.LARGE} containerStyles={[...iconStyle, StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type)]} name={icon.name} - iconID={icon.id} + accountID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} /> diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index d7ddb9ddf9b0..cc36657826f6 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -82,7 +82,7 @@ function SubscriptAvatar({ source={mainAvatar?.source} size={size} name={mainAvatar?.name} - iconID={mainAvatar?.id} + accountID={mainAvatar?.id} type={mainAvatar?.type} fallbackIcon={mainAvatar?.fallbackIcon} /> @@ -109,7 +109,7 @@ function SubscriptAvatar({ size={isSmall ? CONST.AVATAR_SIZE.SMALL_SUBSCRIPT : CONST.AVATAR_SIZE.SUBSCRIPT} fill={secondaryAvatar.fill} name={secondaryAvatar.name} - iconID={secondaryAvatar.id} + accountID={secondaryAvatar.id} type={secondaryAvatar.type} fallbackIcon={secondaryAvatar.fallbackIcon} /> diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx index fa301167a230..a530bcfddc46 100644 --- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx +++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx @@ -59,7 +59,7 @@ function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateA type={icon?.type ?? CONST.ICON_TYPE_AVATAR} name={icon?.name ?? userLogin} fallbackIcon={icon?.fallbackIcon} - iconID={icon?.id} + accountID={icon?.id} /> </View> <Text style={[styles.mt2, styles.textMicroBold, styles.textReactionSenders, styles.textAlignCenter]}>{title}</Text> diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 59c5b0e240ad..ce8d116a0b2d 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -200,7 +200,7 @@ function ReportActionItemSingle({ source={icon.source} type={icon.type} name={icon.name} - iconID={icon.id} + accountID={icon.id} fallbackIcon={fallbackIcon} /> </View> diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index c328bea9bb96..d872cb0275f1 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -83,7 +83,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi fallbackIcon={Expensicons.FallbackWorkspaceAvatar} size={CONST.AVATAR_SIZE.XLARGE} name={policyName} - iconID={policy?.id ?? ''} + accountID={policy?.id ?? ''} type={CONST.ICON_TYPE_WORKSPACE} /> ), diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index 21c14ff8cda4..c9473914eaae 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -151,7 +151,7 @@ function WorkspacesListRow({ source={workspaceIcon} fallbackIcon={fallbackWorkspaceIcon} name={title} - iconID={policyID} + accountID={policyID} type={CONST.ICON_TYPE_WORKSPACE} /> <Text From e3519979e7b69d61f3eba3637ad6eb1af24e65d6 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Wed, 17 Apr 2024 13:00:27 +0700 Subject: [PATCH 164/580] safely get personal detail --- src/components/OptionListContextProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 94fe51711e02..5d4e704262f5 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -121,8 +121,8 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp }> = []; Object.keys(personalDetails).forEach((accoutID) => { - const prevPersonalDetail = prevPersonalDetails.current[accoutID]; - const personalDetail = personalDetails[accoutID]; + const prevPersonalDetail = prevPersonalDetails.current?.[accoutID]; + const personalDetail = personalDetails?.[accoutID]; if (isEqualPersonalDetail(prevPersonalDetail, personalDetail)) { return; From 1fecd977f9b534f119955a62bd4c7ecf31135bd2 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Wed, 17 Apr 2024 13:07:31 +0700 Subject: [PATCH 165/580] update to use usePrevious hook --- src/components/OptionListContextProvider.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 5d4e704262f5..473c0f5bb083 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -51,7 +51,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp }); const personalDetails = usePersonalDetails(); - const prevPersonalDetails = useRef(personalDetails); + const prevPersonalDetails = usePrevious(personalDetails); const prevReports = usePrevious(reports); /** @@ -121,7 +121,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp }> = []; Object.keys(personalDetails).forEach((accoutID) => { - const prevPersonalDetail = prevPersonalDetails.current?.[accoutID]; + const prevPersonalDetail = prevPersonalDetails?.[accoutID]; const personalDetail = personalDetails?.[accoutID]; if (isEqualPersonalDetail(prevPersonalDetail, personalDetail)) { @@ -153,7 +153,6 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp return newOptions; }); - prevPersonalDetails.current = personalDetails; // eslint-disable-next-line react-hooks/exhaustive-deps }, [personalDetails]); From 4d872eb62691bc0f30b5ec0f9605e9a2926691b1 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Wed, 17 Apr 2024 10:10:13 +0200 Subject: [PATCH 166/580] Minor improvements --- src/libs/PolicyUtils.ts | 2 +- src/libs/actions/IOU.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 35abc7258d45..48e0032da8cd 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -11,8 +11,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getPolicyIDFromState from './Navigation/getPolicyIDFromState'; import Navigation, {navigationRef} from './Navigation/Navigation'; import type {RootStackParamList, State} from './Navigation/types'; -import {getPersonalDetailByEmail} from './PersonalDetailsUtils'; import * as NetworkStore from './Network/NetworkStore'; +import {getPersonalDetailByEmail} from './PersonalDetailsUtils'; type MemberEmailsToAccountIDs = Record<string, number>; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ab39072a56b8..9916afbf3a96 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -856,7 +856,7 @@ function buildOnyxDataForInvoice( optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags, isNewChatReport: boolean, transactionThreadReport: OptimisticChatReport, - transactionThreadCreatedReportAction: OptimisticCreatedReportAction, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction | EmptyObject, inviteReportAction?: OptimisticInviteReportAction, policy?: OnyxEntry<OnyxTypes.Policy>, policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, From a9d6b4bd5ad26334c9645abe424f70ed7b94e33e Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Wed, 17 Apr 2024 16:35:14 +0700 Subject: [PATCH 167/580] fix add custom icon right button --- src/components/Button/index.tsx | 20 ++++++++++++------- .../ButtonWithDropdownMenu/index.tsx | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 68f1aac41a5b..25a9fcbc215a 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -115,6 +115,9 @@ type ButtonProps = Partial<ChildrenProps> & { /** Boolean whether to display the right icon */ shouldShowRightIcon?: boolean; + + /** The custom icon to display to the right of the text */ + customRightIcon?: React.ReactNode; }; type KeyboardShortcutComponentProps = Pick<ButtonProps, 'isDisabled' | 'isLoading' | 'onPress' | 'pressOnEnter' | 'allowBubble' | 'enterKeyEventListenerPriority'>; @@ -198,6 +201,7 @@ function Button( id = '', accessibilityLabel = '', + customRightIcon, ...rest }: ButtonProps, ref: ForwardedRef<View | HTMLDivElement>, @@ -253,13 +257,15 @@ function Button( </View> {shouldShowRightIcon && ( <View style={[styles.justifyContentCenter, large ? styles.ml2 : styles.ml1, iconRightStyles]}> - <Icon - src={iconRight} - fill={iconFill ?? (success || danger ? theme.textLight : theme.icon)} - small={small} - medium={medium} - large={large} - /> + {customRightIcon ?? ( + <Icon + src={iconRight} + fill={iconFill ?? (success || danger ? theme.textLight : theme.icon)} + small={small} + medium={medium} + large={large} + /> + )} </View> )} </View> diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 9c2ab4b36734..16c9804f79d0 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -98,7 +98,7 @@ function ButtonWithDropdownMenu<IValueType>({ medium={!isButtonSizeLarge} innerStyles={[innerStyleDropButton, !isSplit && styles.dropDownButtonCartIconView]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} - iconRight={getIconRightButton} + customRightIcon={getIconRightButton()} shouldShowRightIcon={!isSplit} /> From 23dd180b89f7b656b5c2138b49bac1c0a1a4c3ac Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 11:48:47 +0200 Subject: [PATCH 168/580] improve getInvoicesChatName --- src/libs/ReportUtils.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 28edbd79a9ba..4f50562de698 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3011,26 +3011,20 @@ function getInvoicesChatName(report: OnyxEntry<Report>): string { const invoiceReceiver = report?.invoiceReceiver; const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; - const policyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; - let isReceiver = false; - - if (isIndividual && invoiceReceiverAccountID === currentUserAccountID) { - isReceiver = true; - } + const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; + const isCurrentUserReceiver = + (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyEmployee(invoiceReceiverPolicyID, allPolicies)); - if (!isIndividual && PolicyUtils.isPolicyEmployee(policyID, allPolicies)) { - isReceiver = true; - } - - if (isReceiver) { - return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]); + if (isCurrentUserReceiver) { + return getPolicyName(report); } if (isIndividual) { return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]); } - return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]); + // TODO: Check this flow in a scope of the Invoice V0.3 + return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]); } /** From 12bd9c484f46510441726eb5626cb974153b6f5b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 11:52:24 +0200 Subject: [PATCH 169/580] add comment --- src/libs/ReportUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4f50562de698..8a0b1d43adf6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2977,6 +2977,11 @@ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Reco return roomName ? `${verb} ${users} ${preposition} ${roomName}` : `${verb} ${users}`; } +/** + * Get the invoice payer name based on its type: + * - Individual - a receiver display name. + * - Policy - a receiver policy name. + */ function getInvoicePayerName(report: OnyxEntry<Report>): string { const invoiceReceiver = report?.invoiceReceiver; const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; From 7b711429ea0a4ff3a965b531a7097aacd3e945bc Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 11:53:03 +0200 Subject: [PATCH 170/580] Revert "integrate deleted invoice message" This reverts commit 4e81a6c86a7a00ffbaa7692f17cf82d29f0cfb24. --- src/components/ReportActionItem/MoneyRequestAction.tsx | 2 -- src/languages/en.ts | 1 - src/languages/es.ts | 1 - src/pages/home/report/ReportActionItem.tsx | 2 -- 4 files changed, 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index 9bef637bf292..7d9ba2697c7a 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -116,8 +116,6 @@ function MoneyRequestAction({ message = 'parentReportAction.reversedTransaction'; } else if (isTrackExpenseAction) { message = 'parentReportAction.deletedExpense'; - } else if (action.childType === CONST.REPORT.TYPE.INVOICE) { - message = 'parentReportAction.deletedInvoice'; } else { message = 'parentReportAction.deletedRequest'; } diff --git a/src/languages/en.ts b/src/languages/en.ts index b6de47a3aaf0..05313113c521 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2541,7 +2541,6 @@ export default { reversedTransaction: '[Reversed transaction]', deletedTask: '[Deleted task]', hiddenMessage: '[Hidden message]', - deletedInvoice: '[Deleted invoice]', }, threads: { thread: 'Thread', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6b963a874599..f99eaa4eef10 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3033,7 +3033,6 @@ export default { reversedTransaction: '[Transacción anulada]', deletedTask: '[Tarea eliminada]', hiddenMessage: '[Mensaje oculto]', - deletedInvoice: '[Factura eliminada]', }, threads: { thread: 'Hilo', diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 06e222d86928..6a6eca9fb734 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -738,8 +738,6 @@ function ReportActionItem({ message = 'parentReportAction.reversedTransaction'; } else if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) { message = 'parentReportAction.deletedExpense'; - } else if (parentReportAction?.childType === CONST.REPORT.TYPE.INVOICE) { - message = 'parentReportAction.deletedInvoice'; } else { message = 'parentReportAction.deletedRequest'; } From 78f5f642773d697474736dbe2cbcf4eea6384d8b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 11:54:01 +0200 Subject: [PATCH 171/580] Revert "integrate deleted invoice message in getTransactionReportName" This reverts commit 19b54e34b25b9d9ddcb7c27ae4b237cdcac744ff. --- src/libs/ReportUtils.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8a0b1d43adf6..4f8a16246d61 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -191,9 +191,6 @@ type OptimisticIOUReportAction = Pick< | 'receipt' | 'whisperedToAccountIDs' | 'childReportID' - | 'childType' - | 'childVisibleActionCount' - | 'childCommenterCount' >; type ReportRouteParams = { @@ -2691,10 +2688,6 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis return Localize.translateLocal('iou.receiptMissingDetails'); } - if (reportAction?.childType === CONST.REPORT.TYPE.INVOICE) { - return Localize.translateLocal('parentReportAction.deletedInvoice'); - } - const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { From c230799834047354b45cda6f0f00b1d24cafded4 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 11:54:30 +0200 Subject: [PATCH 172/580] clear redundant participant props --- src/types/onyx/Report.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 2bad31bc372c..36f124a4b826 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -26,9 +26,6 @@ type PendingChatMember = { type Participant = { hidden?: boolean; role?: 'admin' | 'member'; - // TODO: Confirm - type?: 'policy' | 'individual'; - policyID?: string; }; type Participants = Record<number, Participant>; From 75f8b2386c9056ec06bd781bb00d61b3afa4ffab Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Wed, 17 Apr 2024 11:58:26 +0200 Subject: [PATCH 173/580] Update optimistic invoice room creation to include current user as a member --- src/libs/actions/IOU.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9916afbf3a96..130a43a79319 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1619,6 +1619,7 @@ function getDeleteTrackExpenseInformation( /** Gathers all the data needed to create an invoice. */ function getSendInvoiceInformation( transaction: OnyxEntry<OnyxTypes.Transaction>, + currentUserAccountID: number, invoiceChatReport?: OnyxEntry<OnyxTypes.Report>, receipt?: Receipt, policy?: OnyxEntry<OnyxTypes.Policy>, @@ -1641,7 +1642,7 @@ function getSendInvoiceInformation( if (!chatReport) { isNewChatReport = true; - chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE, senderWorkspaceID); + chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID, currentUserAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE, senderWorkspaceID); } // STEP 3: Create a new optimistic invoice report. @@ -3341,7 +3342,7 @@ function sendInvoice( optimisticTransactionID, optimisticTransactionThreadReportID, onyxData, - } = getSendInvoiceInformation(transaction, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); + } = getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); let parameters: SendInvoiceParams = { senderWorkspaceID, From 5625582f11ab9b8a4c136651fce4319b09ad6689 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 12:16:41 +0200 Subject: [PATCH 174/580] sync changes --- src/CONST.ts | 3 ++- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/libs/ReportUtils.ts | 32 +++++++++++++++++--------------- src/types/onyx/Report.ts | 23 +++++++++++++---------- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0dbf26851ead..95056970457b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -52,7 +52,7 @@ const chatTypes = { POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', SELF_DM: 'selfDM', - INVOICE: 'invoiceRoom', + INVOICE: 'invoice', } as const; // Explicit type annotation is required @@ -1423,6 +1423,7 @@ const CONST = { SPLIT: 'split', REQUEST: 'request', TRACK_EXPENSE: 'track-expense', + INVOICE: 'invoice', }, REQUEST_TYPE: { DISTANCE: 'distance', diff --git a/src/languages/en.ts b/src/languages/en.ts index 05313113c521..f9c845b42a5a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -505,6 +505,7 @@ export default { beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` to chat about anything ${workspaceName} related.`, beginningOfChatHistoryUserRoomPartOne: 'Collaboration starts here! 🎉\nUse this space to chat about anything ', beginningOfChatHistoryUserRoomPartTwo: ' related.', + beginningOfChatHistoryInvoiceRoom: 'Collaboration starts here! 🎉 Use this room to view, discuss, and pay invoices.', beginningOfChatHistory: 'This is the beginning of your chat with ', beginningOfChatHistoryPolicyExpenseChatPartOne: 'Collaboration between ', beginningOfChatHistoryPolicyExpenseChatPartTwo: ' and ', @@ -522,7 +523,6 @@ export default { // eslint-disable-next-line @typescript-eslint/naming-convention 'track-expense': 'track an expense', }, - beginningOfChatHistoryInvoiceRoom: 'Collaboration starts here! 🎉 \nUse this room to view, discuss, and pay invoices.', }, reportAction: { asCopilot: 'as copilot for', diff --git a/src/languages/es.ts b/src/languages/es.ts index f99eaa4eef10..568e14e6b241 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -501,6 +501,7 @@ export default { beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, beginningOfChatHistoryUserRoomPartOne: '¡Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', beginningOfChatHistoryUserRoomPartTwo: '.', + beginningOfChatHistoryInvoiceRoom: '¡Este es el lugar para colaborar! 🎉 Utilice esta sala para ver, discutir y pagar facturas.', beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ', beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ', beginningOfChatHistoryPolicyExpenseChatPartTwo: ' y ', @@ -518,7 +519,6 @@ export default { // eslint-disable-next-line @typescript-eslint/naming-convention 'track-expense': 'rastrear un gasto', }, - beginningOfChatHistoryInvoiceRoom: '¡Este es el lugar para colaborar! 🎉\nUsa esta sala para ver, discutir y pagar facturas.', }, reportAction: { asCopilot: 'como copiloto de', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4f8a16246d61..76693877b462 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -141,6 +141,7 @@ type OptimisticAddCommentReportAction = Pick< | 'childStatusNum' | 'childStateNum' | 'errors' + | 'whisperedToAccountIDs' > & {isOptimisticAction: boolean}; type OptimisticReportAction = { @@ -283,6 +284,7 @@ type OptimisticChatReport = Pick< | 'description' | 'writeCapability' | 'avatarUrl' + | 'invoiceReceiver' > & { isOptimisticReport: true; }; @@ -675,6 +677,13 @@ function isChatReport(report: OnyxEntry<Report> | EmptyObject): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } +/** + * Checks if a report is an invoice report. + */ +function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { + return report?.type === CONST.REPORT.TYPE.INVOICE; +} + /** * Checks if a report is an Expense report. */ @@ -862,6 +871,13 @@ function isPolicyExpenseChat(report: OnyxEntry<Report> | Participant | EmptyObje return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || (report?.isPolicyExpenseChat ?? false); } +/** + * Whether the provided report is an invoice room chat. + */ +function isInvoiceRoom(report: OnyxEntry<Report>): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; +} + /** * Whether the provided report belongs to a Control policy and is an expense chat */ @@ -906,20 +922,6 @@ function isPaidGroupPolicyExpenseReport(report: OnyxEntry<Report>): boolean { return isExpenseReport(report) && isPaidGroupPolicy(report); } -/** - * Check if Report is an invoice room - */ -function isInvoiceRoom(report: OnyxEntry<Report>): boolean { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; -} - -/** - * Check if Report is an invoice report - */ -function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { - return report?.type === CONST.REPORT.TYPE.INVOICE; -} - /** * Checks if the supplied report is an invoice report in Open state and status. */ @@ -2218,7 +2220,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea function getMoneyRequestSpendBreakdown(report: OnyxEntry<Report>, allReportsDict: OnyxCollection<Report> = null): SpendBreakdown { const allAvailableReports = allReportsDict ?? allReports; let moneyRequestReport; - if (isMoneyRequestReport(report)) { + if (isMoneyRequestReport(report) || isInvoiceReport(report)) { moneyRequestReport = report; } if (allAvailableReports && report?.iouReportID) { diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 36f124a4b826..344b7df5b2eb 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -28,6 +28,16 @@ type Participant = { role?: 'admin' | 'member'; }; +type InvoiceReceiver = + | { + type: 'individual'; + accountID: number; + } + | { + type: 'policy'; + policyID: string; + }; + type Participants = Record<number, Participant>; type Report = OnyxCommon.OnyxValueWithOfflineFeedback< @@ -128,6 +138,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Report cached total */ cachedTotal?: string; + /** Invoice room receiver data */ + invoiceReceiver?: InvoiceReceiver; + lastMessageTranslationKey?: string; parentReportID?: string; parentReportActionID?: string; @@ -186,16 +199,6 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< transactionThreadReportID?: string; fieldList?: Record<string, PolicyReportField>; - - invoiceReceiver?: - | { - type: typeof CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; - accountID: number; - } - | { - type: typeof CONST.INVOICE_RECEIVER_TYPE.POLICY; - policyID: string; - }; }, PolicyReportField['fieldID'] >; From 1b92e52f10d874af91ab346590e315aa640be244 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 12:18:30 +0200 Subject: [PATCH 175/580] update invoice receiver const --- src/CONST.ts | 9 ++++----- src/libs/ReportUtils.ts | 10 +++++----- src/libs/actions/IOU.ts | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 95056970457b..6467769263dd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -839,6 +839,10 @@ const CONST = { OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + BUSINESS: 'policy', + }, }, NEXT_STEP: { FINISHED: 'Finished!', @@ -4353,11 +4357,6 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, - - INVOICE_RECEIVER_TYPE: { - INDIVIDUAL: 'individual', - POLICY: 'policy', - }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 76693877b462..b2a1fd53afeb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1971,7 +1971,7 @@ function getIcons( const invoiceRoomReport = getReport(report.chatReportID); const icons = [getWorkspaceIcon(invoiceRoomReport, policy)]; - if (invoiceRoomReport?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { + if (invoiceRoomReport?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { icons.push(...getIconsForParticipants([invoiceRoomReport?.invoiceReceiver.accountID], personalDetails)); return icons; @@ -2979,7 +2979,7 @@ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Reco */ function getInvoicePayerName(report: OnyxEntry<Report>): string { const invoiceReceiver = report?.invoiceReceiver; - const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; if (isIndividual) { return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiver.accountID]); @@ -3009,7 +3009,7 @@ function getReportActionMessage(reportAction: ReportAction | EmptyObject, parent */ function getInvoicesChatName(report: OnyxEntry<Report>): string { const invoiceReceiver = report?.invoiceReceiver; - const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; const isCurrentUserReceiver = @@ -3032,7 +3032,7 @@ function getInvoicesChatName(report: OnyxEntry<Report>): string { */ function getInvoicesChatSubtitle(report: OnyxEntry<Report>): string { const invoiceReceiver = report?.invoiceReceiver; - const isIndividual = invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; const policyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; let isReceiver = false; @@ -5236,7 +5236,7 @@ function canLeaveRoom(report: OnyxEntry<Report>, isPolicyEmployee: boolean): boo } const isReceiverPolicyAdmin = - report?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.POLICY ? getPolicy(report?.invoiceReceiver?.policyID)?.role === CONST.POLICY.ROLE.ADMIN : false; + report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS ? getPolicy(report?.invoiceReceiver?.policyID)?.role === CONST.POLICY.ROLE.ADMIN : false; if (isReceiverPolicyAdmin) { return false; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 947a800f5ee5..ae93ec7413ac 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5328,7 +5328,7 @@ function canIOUBePaid(iouReport: OnyxEntry<OnyxTypes.Report> | EmptyObject, chat } if (ReportUtils.isInvoiceReport(iouReport)) { - if (chatReport?.invoiceReceiver?.type === CONST.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { + if (chatReport?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return chatReport?.invoiceReceiver?.accountID === userAccountID; } From fb69172a697a9892cc07f7a6a80581f3f3ee99e6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 12:22:27 +0200 Subject: [PATCH 176/580] improve getInvoicesChatSubtitle --- src/libs/ReportUtils.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b2a1fd53afeb..efd51ef4af99 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3034,18 +3034,11 @@ function getInvoicesChatSubtitle(report: OnyxEntry<Report>): string { const invoiceReceiver = report?.invoiceReceiver; const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; - const policyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; - let isReceiver = false; - - if (isIndividual && invoiceReceiverAccountID === currentUserAccountID) { - isReceiver = true; - } - - if (!isIndividual && PolicyUtils.isPolicyEmployee(policyID, allPolicies)) { - isReceiver = true; - } + const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; + const isCurrentUserReceiver = + (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyEmployee(invoiceReceiverPolicyID, allPolicies)); - if (isReceiver) { + if (isCurrentUserReceiver) { let receiver = ''; if (isIndividual) { @@ -3057,7 +3050,8 @@ function getInvoicesChatSubtitle(report: OnyxEntry<Report>): string { return Localize.translateLocal('workspace.invoices.invoicesTo', {receiver}); } - return Localize.translateLocal('workspace.invoices.invoicesFrom', {sender: getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`])}); + // TODO: Check this flow in a scope of the Invoice V0.3 + return Localize.translateLocal('workspace.invoices.invoicesFrom', {sender: getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`])}); } /** From 2610f14920e41ec6a9b067bab9c9c4dab4c7fa77 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 12:25:59 +0200 Subject: [PATCH 177/580] revert extra changes --- src/libs/ReportUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index efd51ef4af99..f41b310c2f2e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -141,7 +141,6 @@ type OptimisticAddCommentReportAction = Pick< | 'childStatusNum' | 'childStateNum' | 'errors' - | 'whisperedToAccountIDs' > & {isOptimisticAction: boolean}; type OptimisticReportAction = { @@ -192,6 +191,8 @@ type OptimisticIOUReportAction = Pick< | 'receipt' | 'whisperedToAccountIDs' | 'childReportID' + | 'childVisibleActionCount' + | 'childCommenterCount' >; type ReportRouteParams = { From b8e9d4368a6d2e90e3114cd8ea291d469a11330f Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 17 Apr 2024 12:31:31 +0200 Subject: [PATCH 178/580] feat: add proper copies --- src/languages/en.ts | 6 ++++++ src/languages/es.ts | 6 ++++++ .../EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx | 2 +- src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx | 2 +- .../PersonalInfo/substeps/SocialSecurityNumber.tsx | 4 ++-- .../utils/getInitialSubstepForPersonalInfo.ts | 4 ++-- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 495ec8426ea8..bc4192735cea 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1702,6 +1702,12 @@ export default { address: 'Address', letsDoubleCheck: "Let's double check that everything looks right.", byAddingThisBankAccount: 'By adding this bank account, you confirm that you have read, understand and accept', + whatsYourLegalName: 'What’s your legal name?', + whatsYourDOB: 'What’s your date of birth?', + whatsYourAddress: 'What’s your address?', + noPOBoxesPlease: 'No PO boxes or mail-drop addresses, please.', + whatsYourSSN: 'What are the last four digits of your Social Security Number?', + noPersonalChecks: 'Don’t worry, no personal credit checks here!', whatsYourPhoneNumber: 'What’s your phone number?', weNeedThisToVerify: 'We need this to verify your wallet.', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 74076c22393f..37c51d2148aa 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1727,6 +1727,12 @@ export default { address: 'Dirección', letsDoubleCheck: 'Revisemos que todo esté bien', byAddingThisBankAccount: 'Añadiendo esta cuenta bancaria, confirmas que has leído, entendido y aceptado', + whatsYourLegalName: '¿Cuál es tu nombre legal?', + whatsYourDOB: '¿Cuál es tu fecha de nacimiento?', + whatsYourAddress: '¿Cuál es tu dirección?', + noPOBoxesPlease: 'Nada de apartados de correos ni direcciones de envío, por favor.', + whatsYourSSN: '¿Cuáles son los últimos 4 dígitos de tu número de la seguridad social?', + noPersonalChecks: 'No te preocupes, no hacemos verificaciones de crédito personales.', whatsYourPhoneNumber: '¿Cuál es tu número de teléfono?', weNeedThisToVerify: 'Necesitamos esto para verificar tu billetera.', }, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx index a42b9c76d1a9..f83edb706686 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx @@ -67,7 +67,7 @@ function DateOfBirth({walletAdditionalDetails, onNext, isEditing}: DateOfBirthPr style={[styles.mh5, styles.flexGrow2, styles.justifyContentBetween]} submitButtonStyles={[styles.pb5, styles.mb0]} > - <Text style={[styles.textHeadlineLineHeightXXL, styles.mb5]}>{translate('personalInfoStep.enterYourDateOfBirth')}</Text> + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb5]}>{translate('personalInfoStep.whatsYourDOB')}</Text> <InputWrapper InputComponent={DatePicker} inputID={PERSONAL_INFO_DOB_KEY} diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx index 89b60b1ba390..32bd1537ce3b 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx @@ -66,7 +66,7 @@ function FullName({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { submitButtonStyles={[styles.pb5, styles.mb0]} > <View> - <Text style={[styles.textHeadlineLineHeightXXL, styles.mb6]}>{translate('personalInfoStep.enterYourLegalFirstAndLast')}</Text> + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb6]}>{translate('personalInfoStep.whatsYourLegalName')}</Text> <View style={[styles.flex2, styles.mb6]}> <InputWrapper InputComponent={TextInput} diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx index 4e82fc3570ed..fe23f4b9cbea 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx @@ -60,8 +60,8 @@ function SocialSecurityNumber({walletAdditionalDetails, onNext, isEditing}: Soci submitButtonStyles={[styles.pb5, styles.mb0]} > <View> - <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.enterTheLast4')}</Text> - <Text style={[styles.textSupporting]}>{translate('personalInfoStep.dontWorry')}</Text> + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.whatsYourSSN')}</Text> + <Text style={[styles.textSupporting]}>{translate('personalInfoStep.noPersonalChecks')}</Text> <View style={[styles.flex1]}> <InputWrapper InputComponent={TextInput} diff --git a/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts b/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts index 82f5567e7f9d..c1063b962d46 100644 --- a/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts +++ b/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts @@ -15,11 +15,11 @@ function getInitialSubstepForPersonalInfo(data: PersonalInfoStepProps): number { return 1; } - if (data[personalInfoKeys.SSN_LAST_4] === '') { + if (data[personalInfoKeys.PHONE_NUMBER] === '') { return 2; } - if (data[personalInfoKeys.PHONE_NUMBER] === '') { + if (data[personalInfoKeys.SSN_LAST_4] === '') { return 3; } From b8d94a7681ffe4d84aa94aa24ca0893d910b449f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 12:32:51 +0200 Subject: [PATCH 179/580] simplify invoice room subtitle --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f41b310c2f2e..417cfdc09bf9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3149,7 +3149,7 @@ function getReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = nu */ function getChatRoomSubtitle(report: OnyxEntry<Report>): string | undefined { if (isInvoiceRoom(report)) { - return getInvoicesChatSubtitle(report); + return Localize.translateLocal('workspace.common.invoices'); } if (isChatThread(report)) { return ''; From 04ee7b1fb55945358135fcb907271a11a34be45e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 12:33:55 +0200 Subject: [PATCH 180/580] Revert "integrate report subtitle for invoice room" This reverts commit a524346f9402a98b9b6900a167c9ca15a1569078. --- src/languages/en.ts | 4 ---- src/languages/es.ts | 4 ---- src/languages/types.ts | 6 ------ src/libs/ReportUtils.ts | 31 +++---------------------------- 4 files changed, 3 insertions(+), 42 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f9c845b42a5a..d2f76ee4ed44 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -30,8 +30,6 @@ import type { GoBackMessageParams, GoToRoomParams, InstantSummaryParams, - InvoicesFromParams, - InvoicesToParams, LocalTimeParams, LoggedInAsParams, LogSizeParams, @@ -2148,8 +2146,6 @@ export default { unlockVBACopy: "You're all set to accept payments by ACH or credit card!", viewUnpaidInvoices: 'View unpaid invoices', sendInvoice: 'Send invoice', - invoicesFrom: ({sender}: InvoicesFromParams) => `Invoices from ${sender}`, - invoicesTo: ({receiver}: InvoicesToParams) => `Invoices to ${receiver}`, }, travel: { unlockConciergeBookingTravel: 'Unlock Concierge travel booking', diff --git a/src/languages/es.ts b/src/languages/es.ts index 568e14e6b241..141e3ad3db91 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -29,8 +29,6 @@ import type { GoBackMessageParams, GoToRoomParams, InstantSummaryParams, - InvoicesFromParams, - InvoicesToParams, LocalTimeParams, LoggedInAsParams, LogSizeParams, @@ -2176,8 +2174,6 @@ export default { unlockVBACopy: '¡Todo listo para recibir pagos por transferencia o con tarjeta!', viewUnpaidInvoices: 'Ver facturas emitidas pendientes', sendInvoice: 'Enviar factura', - invoicesFrom: ({sender}: InvoicesFromParams) => `Facturas de ${sender}`, - invoicesTo: ({receiver}: InvoicesToParams) => `Facturas a ${receiver}`, }, travel: { unlockConciergeBookingTravel: 'Desbloquea la reserva de viajes con Concierge', diff --git a/src/languages/types.ts b/src/languages/types.ts index 8c4d287b7dfc..59e1bfe40af2 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -251,10 +251,6 @@ type ViolationsTaxOutOfPolicyParams = {taxName?: string}; type TaskCreatedActionParams = {title: string}; -type InvoicesFromParams = {sender: string}; - -type InvoicesToParams = {receiver: string}; - /* Translation Object types */ // eslint-disable-next-line @typescript-eslint/no-explicit-any type TranslationBaseValue = string | string[] | ((...args: any[]) => string); @@ -407,6 +403,4 @@ export type { ZipCodeExampleFormatParams, LogSizeParams, HeldRequestParams, - InvoicesFromParams, - InvoicesToParams, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 417cfdc09bf9..0ba38d8f91de 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3028,33 +3028,6 @@ function getInvoicesChatName(report: OnyxEntry<Report>): string { return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]); } -/** - * Get the subtitle for an invoice room. - */ -function getInvoicesChatSubtitle(report: OnyxEntry<Report>): string { - const invoiceReceiver = report?.invoiceReceiver; - const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; - const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; - const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? ''; - const isCurrentUserReceiver = - (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyEmployee(invoiceReceiverPolicyID, allPolicies)); - - if (isCurrentUserReceiver) { - let receiver = ''; - - if (isIndividual) { - receiver = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]); - } else { - receiver = getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiver?.policyID}`]); - } - - return Localize.translateLocal('workspace.invoices.invoicesTo', {receiver}); - } - - // TODO: Check this flow in a scope of the Invoice V0.3 - return Localize.translateLocal('workspace.invoices.invoicesFrom', {sender: getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? ''}`])}); -} - /** * Get the title for a report. */ @@ -3167,6 +3140,9 @@ function getChatRoomSubtitle(report: OnyxEntry<Report>): string | undefined { if (isArchivedRoom(report)) { return report?.oldPolicyName ?? ''; } + if (isInvoiceRoom(report)) { + return Localize.translateLocal('workspace.common.invoices'); + } return getPolicyName(report); } @@ -6208,7 +6184,6 @@ export { getDisplayNamesWithTooltips, getInvoicePayerName, getInvoicesChatName, - getInvoicesChatSubtitle, getReportName, getReport, getReportNotificationPreference, From c135183a053761716647812c8686e1fe6c81295d Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 17 Apr 2024 13:18:35 +0200 Subject: [PATCH 181/580] refactor: make useStepSubmit generic --- .../useReimbursementAccountStepFormSubmit.ts | 39 +++++------------- src/hooks/useStepFormSubmit.ts | 40 +++++++++++++++++++ ...seWalletAdditionalDetailsStepFormSubmit.ts | 27 +++++++++++++ .../PersonalInfo/substeps/Address.tsx | 5 +-- .../PersonalInfo/substeps/DateOfBirth.tsx | 5 +-- .../PersonalInfo/substeps/FullName.tsx | 5 +-- .../PersonalInfo/substeps/PhoneNumber.tsx | 5 +-- .../substeps/SocialSecurityNumber.tsx | 5 +-- 8 files changed, 87 insertions(+), 44 deletions(-) create mode 100644 src/hooks/useStepFormSubmit.ts create mode 100644 src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts diff --git a/src/hooks/useReimbursementAccountStepFormSubmit.ts b/src/hooks/useReimbursementAccountStepFormSubmit.ts index 85a9b21b4d8d..bd17bfaa1234 100644 --- a/src/hooks/useReimbursementAccountStepFormSubmit.ts +++ b/src/hooks/useReimbursementAccountStepFormSubmit.ts @@ -1,46 +1,27 @@ -import {useCallback} from 'react'; -import type {FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; -import * as FormActions from '@userActions/FormActions'; +import type {FormOnyxKeys} from '@components/Form/types'; +import useStepFormSubmit from '@hooks/useStepFormSubmit'; import type {OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {SubStepProps} from './useSubStep/types'; type UseReimbursementAccountStepFormSubmitParams = Pick<SubStepProps, 'onNext'> & { formId?: OnyxFormKey; - fieldIds: Array<FormOnyxKeys<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM | typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>>; + fieldIds: Array<FormOnyxKeys<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>>; shouldSaveDraft: boolean; }; /** * Hook for handling submit method in ReimbursementAccount substeps. * When user is in editing mode we should save values only when user confirm that - * @param formId - ID for particular form * @param onNext - callback * @param fieldIds - field IDs for particular step * @param shouldSaveDraft - if we should save draft values */ -export default function useReimbursementAccountStepFormSubmit({ - formId = ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, - onNext, - fieldIds, - shouldSaveDraft, -}: UseReimbursementAccountStepFormSubmitParams) { - return useCallback( - (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM | typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>) => { - if (shouldSaveDraft) { - const stepValues = fieldIds.reduce( - (acc, key) => ({ - ...acc, - [key]: values[key], - }), - {}, - ); - - FormActions.setDraftValues(formId, stepValues); - } - - onNext(); - }, - [onNext, formId, fieldIds, shouldSaveDraft], - ); +export default function useReimbursementAccountStepFormSubmit({onNext, fieldIds, shouldSaveDraft}: UseReimbursementAccountStepFormSubmitParams) { + return useStepFormSubmit<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>({ + formId: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, + onNext, + fieldIds, + shouldSaveDraft, + }); } diff --git a/src/hooks/useStepFormSubmit.ts b/src/hooks/useStepFormSubmit.ts new file mode 100644 index 000000000000..86d754bf2fc9 --- /dev/null +++ b/src/hooks/useStepFormSubmit.ts @@ -0,0 +1,40 @@ +import {useCallback} from 'react'; +import type {FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; +import * as FormActions from '@userActions/FormActions'; +import type {OnyxFormKey, OnyxFormValuesMapping} from '@src/ONYXKEYS'; +import type {SubStepProps} from './useSubStep/types'; + +type UseStepFormSubmitParams<T extends keyof OnyxFormValuesMapping> = Pick<SubStepProps, 'onNext'> & { + formId: OnyxFormKey; + fieldIds: Array<FormOnyxKeys<T>>; + shouldSaveDraft: boolean; +}; + +/** + * Hook for handling submit method in substeps. + * When user is in editing mode we should save values only when user confirm that + * @param formId - ID for particular form + * @param onNext - callback + * @param fieldIds - field IDs for particular step + * @param shouldSaveDraft - if we should save draft values + */ +export default function useStepFormSubmit<T extends keyof OnyxFormValuesMapping>({formId, onNext, fieldIds, shouldSaveDraft}: UseStepFormSubmitParams<T>) { + return useCallback( + (values: FormOnyxValues<T>) => { + if (shouldSaveDraft) { + const stepValues = fieldIds.reduce( + (acc, key) => ({ + ...acc, + [key]: values[key], + }), + {}, + ); + + FormActions.setDraftValues(formId, stepValues); + } + + onNext(); + }, + [onNext, formId, fieldIds, shouldSaveDraft], + ); +} diff --git a/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts new file mode 100644 index 000000000000..2ca079a506cc --- /dev/null +++ b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts @@ -0,0 +1,27 @@ +import type {FormOnyxKeys} from '@components/Form/types'; +import useStepFormSubmit from '@hooks/useStepFormSubmit'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {SubStepProps} from './useSubStep/types'; + +type UseWalletAdditionalDetailsStepFormSubmitParams = Pick<SubStepProps, 'onNext'> & { + formId?: OnyxFormKey; + fieldIds: Array<FormOnyxKeys<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>>; + shouldSaveDraft: boolean; +}; + +/** + * Hook for handling submit method in WalletAdditionalDetails substeps. + * When user is in editing mode we should save values only when user confirm that + * @param onNext - callback + * @param fieldIds - field IDs for particular step + * @param shouldSaveDraft - if we should save draft values + */ +export default function useWalletAdditionalDetailsStepFormSubmit({onNext, fieldIds, shouldSaveDraft}: UseWalletAdditionalDetailsStepFormSubmitParams) { + return useStepFormSubmit<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>({ + formId: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, + onNext, + fieldIds, + shouldSaveDraft, + }); +} diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx index 445cf11282b1..d63dd7c08f01 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx @@ -6,9 +6,9 @@ import FormProvider from '@components/Form/FormProvider'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; @@ -59,8 +59,7 @@ function Address({walletAdditionalDetails, onNext, isEditing}: AddressProps) { zipCode: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.ZIP_CODE] ?? '', }; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx index f83edb706686..42cfe4ca11de 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx @@ -8,9 +8,9 @@ import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; @@ -51,8 +51,7 @@ function DateOfBirth({walletAdditionalDetails, onNext, isEditing}: DateOfBirthPr const dobDefaultValue = walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? ''; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx index 32bd1537ce3b..6dbe855b2721 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx @@ -8,9 +8,9 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; @@ -49,8 +49,7 @@ function FullName({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { lastName: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', }; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx index b465efe6fe79..9b5f8cf0a611 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx @@ -8,9 +8,9 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; @@ -42,8 +42,7 @@ function PhoneNumber({walletAdditionalDetails, onNext, isEditing}: PhoneNumberPr const defaultPhoneNumber = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.PHONE_NUMBER] ?? ''; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx index fe23f4b9cbea..de9c9ce25938 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx @@ -8,9 +8,9 @@ import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; -import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWalletAdditionalDetailsStepFormSubmit from '@hooks/useWalletAdditionalDetailsStepFormSubmit'; import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; @@ -43,8 +43,7 @@ function SocialSecurityNumber({walletAdditionalDetails, onNext, isEditing}: Soci const defaultSsnLast4 = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? ''; - const handleSubmit = useReimbursementAccountStepFormSubmit({ - formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ fieldIds: STEP_FIELDS, onNext, shouldSaveDraft: isEditing, From 6db22e200e3c0ca660000d34303c52fa6c04acf0 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Wed, 17 Apr 2024 14:26:15 +0200 Subject: [PATCH 182/580] Add INVOICE_RECEIVER_TYPE to consts --- src/CONST.ts | 9 +++++++++ src/types/onyx/Report.ts | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fa66b916ab7c..08ba93053685 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -839,6 +839,10 @@ const CONST = { OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + BUSINESS: 'policy', + }, }, NEXT_STEP: { FINISHED: 'Finished!', @@ -4353,6 +4357,11 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, + + INVOICE_RECEIVER_TYPE: { + INDIVIDUAL: 'individual', + POLICY: 'policy', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 344b7df5b2eb..51e3ce5fb37d 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -30,11 +30,11 @@ type Participant = { type InvoiceReceiver = | { - type: 'individual'; + type: typeof CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; accountID: number; } | { - type: 'policy'; + type: typeof CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS; policyID: string; }; From 173432f67cb35058def61c27c6ab78d90b6087d2 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Wed, 17 Apr 2024 14:37:20 +0200 Subject: [PATCH 183/580] Code improvements --- src/CONST.ts | 5 -- ...raryForRefactorRequestConfirmationList.tsx | 5 +- src/libs/actions/IOU.ts | 67 +++++++++---------- 3 files changed, 32 insertions(+), 45 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 08ba93053685..6467769263dd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4357,11 +4357,6 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, - - INVOICE_RECEIVER_TYPE: { - INDIVIDUAL: 'individual', - POLICY: 'policy', - }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index e41c6cc96085..7b444be18da9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -256,10 +256,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant?.policyID}`]; }, [allPolicies, pickedParticipants]); - const canUpdateSenderWorkspace = useMemo( - () => PolicyUtils.getActiveAdminWorkspaces(allPolicies).length > 0 && !!transaction?.isFromGlobalCreate, - [allPolicies, transaction?.isFromGlobalCreate], - ); + const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.canSendInvoice(allPolicies) && !!transaction?.isFromGlobalCreate, [allPolicies, transaction?.isFromGlobalCreate]); // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 130a43a79319..d6be557b8107 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -863,24 +863,7 @@ function buildOnyxDataForInvoice( policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); - const optimisticData: OnyxUpdate[] = []; - - if (chatReport) { - optimisticData.push({ - // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page - onyxMethod: isNewChatReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: { - ...chatReport, - lastReadTime: DateUtils.getDBTime(), - lastMessageTranslationKey: '', - iouReportID: iouReport.reportID, - ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), - }, - }); - } - - optimisticData.push( + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -950,7 +933,22 @@ function buildOnyxDataForInvoice( key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, value: null, }, - ); + ]; + + if (chatReport) { + optimisticData.push({ + // Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page + onyxMethod: isNewChatReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + ...chatReport, + lastReadTime: DateUtils.getDBTime(), + lastMessageTranslationKey: '', + iouReportID: iouReport.reportID, + ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), + }, + }); + } if (optimisticPolicyRecentlyUsedCategories.length) { optimisticData.push({ @@ -968,21 +966,7 @@ function buildOnyxDataForInvoice( }); } - const successData: OnyxUpdate[] = []; - - if (isNewChatReport) { - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: { - pendingFields: null, - errorFields: null, - isOptimisticReport: false, - }, - }); - } - - successData.push( + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -1007,7 +991,6 @@ function buildOnyxDataForInvoice( pendingFields: clearedPendingFields, }, }, - { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, @@ -1049,7 +1032,19 @@ function buildOnyxDataForInvoice( }, }, }, - ); + ]; + + if (isNewChatReport) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + pendingFields: null, + errorFields: null, + isOptimisticReport: false, + }, + }); + } const errorKey = DateUtils.getMicroseconds(); From c711542ba80bfc705bad89ade38209adc5a2b135 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Wed, 17 Apr 2024 21:58:33 +0800 Subject: [PATCH 184/580] correctly access the report data --- src/libs/TaskUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TaskUtils.ts b/src/libs/TaskUtils.ts index 19e1025a09c8..188b8f5b31ae 100644 --- a/src/libs/TaskUtils.ts +++ b/src/libs/TaskUtils.ts @@ -38,7 +38,7 @@ function getTaskReportActionMessage(action: OnyxEntry<ReportAction>): Pick<Messa } function getTaskTitle(taskReportID: string, fallbackTitle = ''): string { - const taskReport = allReports?.[taskReportID] ?? {}; + const taskReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`] ?? {}; // We need to check for reportID, not just reportName, because when a receiver opens the task for the first time, // an optimistic report is created with the only property – reportName: 'Chat report', // and it will be displayed as the task title without checking for reportID to be present. From 26bcce68b747dde6bf8c26f73b5207d708004782 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 16:00:02 +0200 Subject: [PATCH 185/580] add permissions to report --- src/CONST.ts | 6 ++++++ src/types/onyx/Report.ts | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 2b85bcb2c326..425391f6ebea 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -837,6 +837,12 @@ const CONST = { OWNER_EMAIL_FAKE: '__FAKE__', OWNER_ACCOUNT_ID_FAKE: 0, DEFAULT_REPORT_NAME: 'Chat Report', + PERMISSIONS: { + READ: 'read', + WRITE: 'write', + SHARE: 'share', + OWN: 'own', + } }, NEXT_STEP: { FINISHED: 'Finished!', diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index fe3eec6dc11e..36d345b1084c 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -186,6 +186,8 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< transactionThreadReportID?: string; fieldList?: Record<string, PolicyReportField>; + + permissions?: Array<ValueOf<typeof CONST.REPORT.PERMISSIONS>>; }, PolicyReportField['fieldID'] >; From de29862ae77c1cd6b5fd2fa03fc8e0443ed133ed Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 16:01:28 +0200 Subject: [PATCH 186/580] create isReadOnly --- src/libs/ReportUtils.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 39964bfcc4d7..6b9ef9031cf0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5989,6 +5989,13 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyID: s return isChatRoom(report) && !isThread(report); } +/** + * + * Checks if report is in read-only mode. + */ +function isReadOnly(report: OnyxEntry<Report>): boolean { + return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE) ?? false; +} export { getReportParticipantsTitle, @@ -6228,6 +6235,7 @@ export { buildParticipantsFromAccountIDs, canReportBeMentionedWithinPolicy, getAllHeldTransactions, + isReadOnly, }; export type { From 7a13ae3e44fc50e05432badaf9b3db0025275ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Wed, 17 Apr 2024 16:01:32 +0200 Subject: [PATCH 187/580] Change caret position styles --- src/components/Composer/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index a8737fdac47a..59f7333433bb 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -289,13 +289,7 @@ function Composer( opacity: 0, }} > - <Text - style={[ - StyleSheet.flatten([style, styles.noSelect]), - (textInput.current?.clientHeight ?? 0) < 330 ? styles.overflowHidden : {}, - {maxWidth: textInputWidth as DimensionValue}, - ]} - > + <Text style={[StyleSheet.flatten([style, styles.noSelect]), StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), {maxWidth: textInputWidth as DimensionValue}]}> {`${valueBeforeCaret} `} <Text numberOfLines={1} From e2ad69dc9610992ac13438b4316c2f2bbe53d1a2 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 16:08:32 +0200 Subject: [PATCH 188/580] integrate OnboardingReportFooterMessage into ReportFooter --- src/libs/ReportUtils.ts | 17 +++++++++-------- .../report/OnboardingReportFooterMessage.tsx | 11 +++++++---- src/pages/home/report/ReportFooter.tsx | 6 ++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6b9ef9031cf0..e20686e1326a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5198,6 +5198,14 @@ function isMoneyRequestReportPendingDeletion(report: OnyxEntry<Report> | EmptyOb return parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } +/** + * + * Checks if report is in read-only mode. + */ +function isReadOnly(report: OnyxEntry<Report>): boolean { + return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE) ?? false; +} + function canUserPerformWriteAction(report: OnyxEntry<Report>) { const reportErrors = getAddWorkspaceRoomOrChatReportErrors(report); @@ -5206,7 +5214,7 @@ function canUserPerformWriteAction(report: OnyxEntry<Report>) { return false; } - return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser; + return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser && !isReadOnly(report); } /** @@ -5989,13 +5997,6 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyID: s return isChatRoom(report) && !isThread(report); } -/** - * - * Checks if report is in read-only mode. - */ -function isReadOnly(report: OnyxEntry<Report>): boolean { - return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE) ?? false; -} export { getReportParticipantsTitle, diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index a48a609b51a7..f6c91d3b9f12 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -1,8 +1,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; @@ -16,8 +15,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy as PolicyType, Report} from '@src/types/onyx'; -type OnboardingReportFooterMessageOnyxProps = {reports: OnyxCollection<Report>; policies: OnyxCollection<PolicyType>}; -type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps & {choice: ValueOf<typeof CONST.INTRO_CHOICES>}; +// TODO: Use a proper choice type +type OnboardingReportFooterMessageOnyxProps = {choice: OnyxEntry<string>; reports: OnyxCollection<Report>; policies: OnyxCollection<PolicyType>}; +type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps; function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingReportFooterMessageProps) { const {translate} = useLocalize(); @@ -83,6 +83,9 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe } OnboardingReportFooterMessage.displayName = 'OnboardingReportFooterMessage'; export default withOnyx<OnboardingReportFooterMessageProps, OnboardingReportFooterMessageOnyxProps>({ + choice: { + key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, + }, reports: { key: ONYXKEYS.COLLECTION.REPORT, }, diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index bd143f9ef196..bccec0597fb0 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -20,6 +20,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import OnboardingReportFooterMessage from './OnboardingReportFooterMessage'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; type ReportFooterOnyxProps = { @@ -81,6 +82,7 @@ function ReportFooter({ const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(report); + const isReadOnlyReport = ReportUtils.isReadOnly(report); const allPersonalDetails = usePersonalDetails(); @@ -125,6 +127,10 @@ function ReportFooter({ [report.reportID, handleCreateTask], ); + if (isReadOnlyReport) { + <OnboardingReportFooterMessage />; + } + return ( <> {hideComposer && ( From 471a02adff994ef218ec3128614a92534fa7f6c4 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 16:09:13 +0200 Subject: [PATCH 189/580] prettify --- src/pages/home/report/OnboardingReportFooterMessage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index f6c91d3b9f12..54659c14cf6e 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -81,7 +81,9 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe </View> ); } + OnboardingReportFooterMessage.displayName = 'OnboardingReportFooterMessage'; + export default withOnyx<OnboardingReportFooterMessageProps, OnboardingReportFooterMessageOnyxProps>({ choice: { key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, From b4aed0fa5eb550664687f06008359fc21bb881c3 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 16:10:17 +0200 Subject: [PATCH 190/580] restrict task action in header --- src/components/TaskHeaderActionButton.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 2d964f58c253..a7e3abcd3012 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -25,6 +25,10 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) const {translate} = useLocalize(); const styles = useThemeStyles(); + if (ReportUtils.isReadOnly(report)) { + return null; + } + return ( <View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd]}> <Button From 0c1efc36f0c9edb66941aa6b41d1961ae42036ed Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 16:12:16 +0200 Subject: [PATCH 191/580] read only for TaskView --- src/components/ReportActionItem/TaskView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 9711e126907f..550b9fc6b05c 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -60,7 +60,7 @@ function TaskView({report, shouldShowHorizontalRule, ...props}: TaskViewProps) { const isOpen = ReportUtils.isOpenTaskReport(report); const canModifyTask = Task.canModifyTask(report, props.currentUserPersonalDetails.accountID); const disableState = !canModifyTask; - const isDisableInteractive = !canModifyTask || !isOpen; + const isDisableInteractive = !canModifyTask || !isOpen || ReportUtils.isReadOnly(report); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const {translate} = useLocalize(); From d72ceab7f79bc0996d42ee2f75665a77c77aa409 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 16:14:00 +0200 Subject: [PATCH 192/580] read only for TaskPreview --- src/components/ReportActionItem/TaskPreview.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 8e8b3b930be7..b396816b024a 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -76,6 +76,7 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR const htmlForTaskPreview = taskAssigneeAccountID !== 0 ? `<comment><mention-user accountid="${taskAssigneeAccountID}"></mention-user> ${taskTitle}</comment>` : `<comment>${taskTitle}</comment>`; const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action); + const isReadOnly = ReportUtils.isReadOnly(taskReport); if (isDeletedParentAction) { return <RenderHTML html={`<comment>${translate('parentReportAction.deletedTask')}</comment>`} />; @@ -98,8 +99,12 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR style={[styles.mr2]} containerStyle={[styles.taskCheckbox]} isChecked={isTaskCompleted} - disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID)} + disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID) && !isReadOnly} onPress={Session.checkIfActionIsAllowed(() => { + if (isReadOnly) { + return; + } + if (isTaskCompleted) { Task.reopenTask(taskReport); } else { From 287fbe1e84673242ce6ba014eaa717fdf37fcd5f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Wed, 17 Apr 2024 16:15:11 +0200 Subject: [PATCH 193/580] read only for HeaderView --- src/pages/home/HeaderView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 56828bce7847..e6bb13b1e3e7 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -130,7 +130,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction, } // Task is not closed - if (report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { + if (!ReportUtils.isReadOnly(report) && report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { threeDotMenuItems.push({ icon: Expensicons.Trashcan, text: translate('common.delete'), From 4718b8052241d5ae66069425073792e647b7eb8a Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 17 Apr 2024 17:40:55 +0200 Subject: [PATCH 194/580] refactor: finishing touches --- ...seWalletAdditionalDetailsStepFormSubmit.ts | 2 +- src/libs/actions/Wallet.ts | 5 ++ .../PersonalInfo/PersonalInfo.tsx | 88 ++++++++----------- .../PersonalInfo/substeps/Address.tsx | 8 +- .../PersonalInfo/substeps/Confirmation.tsx | 13 +-- .../PersonalInfo/substeps/DateOfBirth.tsx | 8 +- .../PersonalInfo/substeps/FullName.tsx | 16 ++-- .../PersonalInfo/substeps/PhoneNumber.tsx | 8 +- .../substeps/SocialSecurityNumber.tsx | 12 +-- .../utils/getInitialSubstepForPersonalInfo.ts | 4 +- .../EnablePayments/utils/getSubstepValues.ts | 17 ++-- src/types/form/PersonalBankAccountForm.ts | 29 +----- src/types/form/WalletAdditionalDetailsForm.ts | 36 ++++++++ src/types/form/index.ts | 1 + src/types/onyx/WalletAdditionalDetails.ts | 26 +++--- 15 files changed, 142 insertions(+), 131 deletions(-) create mode 100644 src/types/form/WalletAdditionalDetailsForm.ts diff --git a/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts index 2ca079a506cc..24fcc88b505c 100644 --- a/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts +++ b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts @@ -19,7 +19,7 @@ type UseWalletAdditionalDetailsStepFormSubmitParams = Pick<SubStepProps, 'onNext */ export default function useWalletAdditionalDetailsStepFormSubmit({onNext, fieldIds, shouldSaveDraft}: UseWalletAdditionalDetailsStepFormSubmitParams) { return useStepFormSubmit<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>({ - formId: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, + formId: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, onNext, fieldIds, shouldSaveDraft, diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index 097d9ee0419a..045cc34f39ef 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -295,6 +295,10 @@ function requestPhysicalExpensifyCard(cardID: number, authToken: string, private API.write(WRITE_COMMANDS.REQUEST_PHYSICAL_EXPENSIFY_CARD, requestParams, {optimisticData}); } +function resetWalletAdditionalDetailsDraft() { + Onyx.set(ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, null); +} + export { openOnfidoFlow, openInitialSettingsPage, @@ -309,4 +313,5 @@ export { acceptWalletTerms, setKYCWallSource, requestPhysicalExpensifyCard, + resetWalletAdditionalDetailsDraft, }; diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 92b005ab8a88..61957ee2865b 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo} from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -6,20 +6,20 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; -import {parsePhoneNumber} from '@libs/PhoneNumber'; +// TODO: uncomment in the next PR +// import {parsePhoneNumber} from '@libs/PhoneNumber'; +import Navigation from '@navigation/Navigation'; import PhoneNumber from '@pages/EnablePayments/PersonalInfo/substeps/PhoneNumber'; import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalBankAccountForm} from '@src/types/form/PersonalBankAccountForm'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {UserWallet, WalletAdditionalDetails} from '@src/types/onyx'; +import type {WalletAdditionalDetailsForm} from '@src/types/form'; +import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; import getInitialSubstepForPersonalInfo from '../utils/getInitialSubstepForPersonalInfo'; import getSubstepValues from '../utils/getSubstepValues'; import Address from './substeps/Address'; @@ -29,14 +29,11 @@ import FullName from './substeps/FullName'; import SocialSecurityNumber from './substeps/SocialSecurityNumber'; type PersonalInfoPageOnyxProps = { - /** The user's wallet */ - userWallet: OnyxEntry<UserWallet>; - /** Reimbursement account from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; /** The draft values of the bank account being setup */ - walletAdditionalDetailsDraft: OnyxEntry<PersonalBankAccountForm>; + walletAdditionalDetailsDraft: OnyxEntry<WalletAdditionalDetailsForm>; }; type PersonalInfoPageProps = PersonalInfoPageOnyxProps; @@ -44,44 +41,29 @@ type PersonalInfoPageProps = PersonalInfoPageOnyxProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, PhoneNumber, SocialSecurityNumber, Address, Confirmation]; -function PersonalInfoPage({userWallet, walletAdditionalDetails, walletAdditionalDetailsDraft}: PersonalInfoPageProps) { +function PersonalInfoPage({walletAdditionalDetails, walletAdditionalDetailsDraft}: PersonalInfoPageProps) { const {translate} = useLocalize(); - const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const {isPendingOnfidoResult, hasFailedOnfido} = userWallet ?? {}; - const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, walletAdditionalDetailsDraft, walletAdditionalDetails), [walletAdditionalDetails, walletAdditionalDetailsDraft]); const submit = () => { - const personalDetails = { - phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', - legalFirstName: values?.[PERSONAL_INFO_STEP_KEYS.FIRST_NAME] ?? '', - legalLastName: values?.[PERSONAL_INFO_STEP_KEYS.LAST_NAME] ?? '', - addressStreet: values?.[PERSONAL_INFO_STEP_KEYS.STREET] ?? '', - addressCity: values?.[PERSONAL_INFO_STEP_KEYS.CITY] ?? '', - addressState: values?.[PERSONAL_INFO_STEP_KEYS.STATE] ?? '', - addressZip: values?.[PERSONAL_INFO_STEP_KEYS.ZIP_CODE] ?? '', - dob: values?.[PERSONAL_INFO_STEP_KEYS.DOB] ?? '', - ssn: values?.[PERSONAL_INFO_STEP_KEYS.SSN_LAST_4] ?? '', - }; + // TODO: uncomment in the next PR + // const personalDetails = { + // phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', + // legalFirstName: values?.[PERSONAL_INFO_STEP_KEYS.FIRST_NAME] ?? '', + // legalLastName: values?.[PERSONAL_INFO_STEP_KEYS.LAST_NAME] ?? '', + // addressStreet: values?.[PERSONAL_INFO_STEP_KEYS.STREET] ?? '', + // addressCity: values?.[PERSONAL_INFO_STEP_KEYS.CITY] ?? '', + // addressState: values?.[PERSONAL_INFO_STEP_KEYS.STATE] ?? '', + // addressZip: values?.[PERSONAL_INFO_STEP_KEYS.ZIP_CODE] ?? '', + // dob: values?.[PERSONAL_INFO_STEP_KEYS.DOB] ?? '', + // ssn: values?.[PERSONAL_INFO_STEP_KEYS.SSN_LAST_4] ?? '', + // }; // Attempt to set the personal details - Wallet.updatePersonalDetails(personalDetails); + // Wallet.updatePersonalDetails(personalDetails); + Navigation.navigate(ROUTES.SETTINGS_WALLET); }; - useEffect(() => { - if (isOffline) { - return; - } - - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (isPendingOnfidoResult || hasFailedOnfido) { - Navigation.navigate(ROUTES.SETTINGS_WALLET, CONST.NAVIGATION.TYPE.UP); - return; - } - - Wallet.openEnablePaymentsPage(); - }, [isOffline, isPendingOnfidoResult, hasFailedOnfido]); - const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(values), [values]); const { @@ -90,21 +72,32 @@ function PersonalInfoPage({userWallet, walletAdditionalDetails, walletAdditional nextScreen, prevScreen, moveTo, + screenIndex, } = useSubStep({ bodyContent, startFrom, onFinished: submit, }); + const handleBackButtonPress = () => { + // TODO: connect to the fist step of the wallet setup + if (screenIndex === 0) { + Navigation.navigate(ROUTES.SETTINGS_WALLET); + Wallet.resetWalletAdditionalDetailsDraft(); + return; + } + prevScreen(); + }; + return ( <ScreenWrapper - shouldShowOfflineIndicator={userWallet?.currentStep !== CONST.WALLET.STEP.ONFIDO} + shouldShowOfflineIndicator={false} includeSafeAreaPaddingBottom={false} testID={PersonalInfoPage.displayName} > <HeaderWithBackButton title={translate('personalInfoStep.personalInfo')} - onBackButtonPress={prevScreen} + onBackButtonPress={handleBackButtonPress} /> <View style={[styles.ph5, styles.mb5, styles.mt3, {height: CONST.BANK_ACCOUNT.STEPS_HEADER_HEIGHT}]}> <InteractiveStepSubHeader @@ -124,13 +117,6 @@ function PersonalInfoPage({userWallet, walletAdditionalDetails, walletAdditional PersonalInfoPage.displayName = 'PersonalInfoPage'; export default withOnyx<PersonalInfoPageProps, PersonalInfoPageOnyxProps>({ - userWallet: { - key: ONYXKEYS.USER_WALLET, - - // We want to refresh the wallet each time the user attempts to activate the wallet so we won't use the - // stored values here. - initWithStoredValues: false, - }, // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx index d63dd7c08f01..f0e240274d3d 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx @@ -13,12 +13,12 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {WalletAdditionalDetails} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; type AddressOnyxProps = { /** wallet additional details from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; }; type AddressProps = AddressOnyxProps & SubStepProps; @@ -92,7 +92,7 @@ function Address({walletAdditionalDetails, onNext, isEditing}: AddressProps) { Address.displayName = 'Address'; export default withOnyx<AddressProps, AddressOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx index 49caca27db9f..f97e8035775a 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx @@ -16,16 +16,17 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {WalletAdditionalDetails} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsForm} from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; import getSubstepValues from '../../utils/getSubstepValues'; type ConfirmationOnyxProps = { /** wallet additional details from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; /** The draft values of the bank account being setup */ - walletAdditionalDetailsDraft: OnyxEntry<WalletAdditionalDetails>; + walletAdditionalDetailsDraft: OnyxEntry<WalletAdditionalDetailsForm>; }; type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; @@ -143,11 +144,11 @@ function Confirmation({walletAdditionalDetails, walletAdditionalDetailsDraft, on Confirmation.displayName = 'Confirmation'; export default withOnyx<ConfirmationProps, ConfirmationOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetailsDraft: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, }, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx index 42cfe4ca11de..8abcb2715e0d 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx @@ -15,12 +15,12 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {WalletAdditionalDetails} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; type DateOfBirthOnyxProps = { /** Reimbursement account from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; }; type DateOfBirthProps = DateOfBirthOnyxProps & SubStepProps; @@ -85,7 +85,7 @@ function DateOfBirth({walletAdditionalDetails, onNext, isEditing}: DateOfBirthPr DateOfBirth.displayName = 'DateOfBirth'; export default withOnyx<DateOfBirthProps, DateOfBirthOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx index 6dbe855b2721..a1a1fd0f45f4 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx @@ -15,12 +15,12 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {WalletAdditionalDetails} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; type FullNameOnyxProps = { /** Wallet Additional Details from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; }; type FullNameProps = FullNameOnyxProps & SubStepProps; @@ -30,12 +30,12 @@ const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.L const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - if (values.firstName && !ValidationUtils.isValidLegalName(values.firstName)) { - errors.firstName = 'bankAccount.error.firstName'; + if (values.legalFirstName && !ValidationUtils.isValidLegalName(values.legalFirstName)) { + errors.legalFirstName = 'bankAccount.error.firstName'; } - if (values.lastName && !ValidationUtils.isValidLegalName(values.lastName)) { - errors.lastName = 'bankAccount.error.lastName'; + if (values.legalLastName && !ValidationUtils.isValidLegalName(values.legalLastName)) { + errors.legalLastName = 'bankAccount.error.lastName'; } return errors; }; @@ -97,7 +97,7 @@ function FullName({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { FullName.displayName = 'FullName'; export default withOnyx<FullNameProps, FullNameOnyxProps>({ - // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + // @ts-expect-error: ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx index 9b5f8cf0a611..095df6f573db 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx @@ -15,12 +15,12 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {WalletAdditionalDetails} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; type PhoneNumberOnyxProps = { /** Reimbursement account from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; }; type PhoneNumberProps = PhoneNumberOnyxProps & SubStepProps; @@ -83,7 +83,7 @@ function PhoneNumber({walletAdditionalDetails, onNext, isEditing}: PhoneNumberPr PhoneNumber.displayName = 'PhoneNumber'; export default withOnyx<PhoneNumberProps, PhoneNumberOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx index de9c9ce25938..5f9f2756927f 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx @@ -15,12 +15,12 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {WalletAdditionalDetails} from '@src/types/onyx'; +import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; type SocialSecurityNumberOnyxProps = { /** Reimbursement account from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>; + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; }; type SocialSecurityNumberProps = SocialSecurityNumberOnyxProps & SubStepProps; @@ -31,8 +31,8 @@ const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS>): FormInputErrors<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS> => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - if (values.ssnLast4 && !ValidationUtils.isValidSSNLastFour(values.ssnLast4)) { - errors.ssnLast4 = 'bankAccount.error.ssnLast4'; + if (values.ssn && !ValidationUtils.isValidSSNLastFour(values.ssn)) { + errors.ssn = 'bankAccount.error.ssnLast4'; } return errors; @@ -84,7 +84,7 @@ function SocialSecurityNumber({walletAdditionalDetails, onNext, isEditing}: Soci SocialSecurityNumber.displayName = 'SocialSecurityNumber'; export default withOnyx<SocialSecurityNumberProps, SocialSecurityNumberOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, diff --git a/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts b/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts index c1063b962d46..74da50570795 100644 --- a/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts +++ b/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts @@ -1,5 +1,5 @@ -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {PersonalInfoStepProps} from '@src/types/form/PersonalBankAccountForm'; +import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {PersonalInfoStepProps} from '@src/types/form/WalletAdditionalDetailsForm'; const personalInfoKeys = INPUT_IDS.PERSONAL_INFO_STEP; diff --git a/src/pages/EnablePayments/utils/getSubstepValues.ts b/src/pages/EnablePayments/utils/getSubstepValues.ts index d78c081197c9..d1fdda5fcbc1 100644 --- a/src/pages/EnablePayments/utils/getSubstepValues.ts +++ b/src/pages/EnablePayments/utils/getSubstepValues.ts @@ -1,18 +1,19 @@ import type {OnyxEntry} from 'react-native-onyx'; -import type {PersonalBankAccountForm} from '@src/types/form'; -import type {WalletAdditionalDetails} from '@src/types/onyx'; +import type {WalletAdditionalDetailsForm} from '@src/types/form'; +import type {PersonalInfoStepProps} from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; -function getSubstepValues<T extends keyof PersonalBankAccountForm>( +function getSubstepValues<T extends keyof WalletAdditionalDetailsForm>( inputKeys: Record<string, T>, - walletAdditionalDetailsDraft: OnyxEntry<PersonalBankAccountForm>, - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetails>, -): {[K in T]: PersonalBankAccountForm[K]} { + walletAdditionalDetailsDraft: OnyxEntry<WalletAdditionalDetailsForm>, + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>, +): {[K in T]: WalletAdditionalDetailsForm[K]} { return Object.entries(inputKeys).reduce( (acc, [, value]) => ({ ...acc, - [value]: walletAdditionalDetailsDraft?.[value] ?? walletAdditionalDetails?.[value] ?? '', + [value]: walletAdditionalDetailsDraft?.[value] ?? walletAdditionalDetails?.[value as keyof PersonalInfoStepProps] ?? '', }), - {} as {[K in T]: PersonalBankAccountForm[K]}, + {} as {[K in T]: WalletAdditionalDetailsForm[K]}, ); } diff --git a/src/types/form/PersonalBankAccountForm.ts b/src/types/form/PersonalBankAccountForm.ts index 8d0ff4f3869c..1a1922bf8013 100644 --- a/src/types/form/PersonalBankAccountForm.ts +++ b/src/types/form/PersonalBankAccountForm.ts @@ -13,18 +13,6 @@ const INPUT_IDS = { PLAID_ACCESS_TOKEN: 'plaidAccessToken', SELECTED_PLAID_ACCOUNT_ID: 'selectedPlaidAccountID', }, - PERSONAL_INFO_STEP: { - FIRST_NAME: 'firstName', - LAST_NAME: 'lastName', - DOB: 'dob', - SSN_LAST_4: 'ssnLast4', - STREET: 'addressStreet', - CITY: 'addressCity', - STATE: 'addressState', - ZIP_CODE: 'addressZipCode', - PHONE_NUMBER: 'phoneNumber', - IS_ONFIDO_SETUP_COMPLETE: 'isOnfidoSetupComplete', - }, } as const; type InputID = DeepValueOf<typeof INPUT_IDS>; @@ -37,19 +25,6 @@ type BankAccountStepProps = { [INPUT_IDS.BANK_INFO_STEP.SETUP_TYPE]: string; }; -type PersonalInfoStepProps = { - [INPUT_IDS.PERSONAL_INFO_STEP.FIRST_NAME]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.LAST_NAME]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.STREET]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.CITY]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.STATE]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.ZIP_CODE]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.DOB]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.PHONE_NUMBER]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.SSN_LAST_4]: string; - [INPUT_IDS.PERSONAL_INFO_STEP.IS_ONFIDO_SETUP_COMPLETE]: boolean; -}; - type PlaidAccountProps = { [INPUT_IDS.BANK_INFO_STEP.IS_SAVINGS]: boolean; [INPUT_IDS.BANK_INFO_STEP.BANK_NAME]: string; @@ -57,8 +32,8 @@ type PlaidAccountProps = { [INPUT_IDS.BANK_INFO_STEP.SELECTED_PLAID_ACCOUNT_ID]: string; }; -type PersonalBankAccountForm = Form<InputID, PersonalInfoStepProps & BankAccountStepProps & PlaidAccountProps>; +type PersonalBankAccountForm = Form<InputID, BankAccountStepProps & PlaidAccountProps>; -export type {BankAccountStepProps, PlaidAccountProps, PersonalInfoStepProps, PersonalBankAccountForm}; +export type {BankAccountStepProps, PlaidAccountProps, PersonalBankAccountForm}; export default INPUT_IDS; diff --git a/src/types/form/WalletAdditionalDetailsForm.ts b/src/types/form/WalletAdditionalDetailsForm.ts new file mode 100644 index 000000000000..f3b167b09a14 --- /dev/null +++ b/src/types/form/WalletAdditionalDetailsForm.ts @@ -0,0 +1,36 @@ +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type Form from './Form'; + +const INPUT_IDS = { + PERSONAL_INFO_STEP: { + FIRST_NAME: 'legalFirstName', + LAST_NAME: 'legalLastName', + DOB: 'dob', + SSN_LAST_4: 'ssn', + STREET: 'addressStreet', + CITY: 'addressCity', + STATE: 'addressState', + ZIP_CODE: 'addressZipCode', + PHONE_NUMBER: 'phoneNumber', + }, +} as const; + +type InputID = DeepValueOf<typeof INPUT_IDS>; + +type PersonalInfoStepProps = { + [INPUT_IDS.PERSONAL_INFO_STEP.FIRST_NAME]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.LAST_NAME]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.STREET]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.CITY]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.STATE]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.ZIP_CODE]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.DOB]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.PHONE_NUMBER]: string; + [INPUT_IDS.PERSONAL_INFO_STEP.SSN_LAST_4]: string; +}; + +type WalletAdditionalDetailsForm = Form<InputID, PersonalInfoStepProps>; + +export type {PersonalInfoStepProps, WalletAdditionalDetailsForm}; + +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index ce3fcd428999..e68d0b4b50af 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -47,5 +47,6 @@ export type {WorkspaceTaxValueForm} from './WorkspaceTaxValueForm'; export type {WorkspaceTaxCustomName} from './WorkspaceTaxCustomName'; export type {PolicyCreateDistanceRateForm} from './PolicyCreateDistanceRateForm'; export type {PolicyDistanceRateEditForm} from './PolicyDistanceRateEditForm'; +export type {WalletAdditionalDetailsForm} from './WalletAdditionalDetailsForm'; export type {NewChatNameForm} from './NewChatNameForm'; export type {default as Form} from './Form'; diff --git a/src/types/onyx/WalletAdditionalDetails.ts b/src/types/onyx/WalletAdditionalDetails.ts index f2a9d10b1293..c574006e9f66 100644 --- a/src/types/onyx/WalletAdditionalDetails.ts +++ b/src/types/onyx/WalletAdditionalDetails.ts @@ -6,6 +6,18 @@ type WalletAdditionalQuestionDetails = { answer: string[]; }; +type WalletPersonalDetails = { + legalFirstName: string; + legalLastName: string; + dob: string; + ssn: string; + addressStreet: string; + addressCity: string; + addressState: string; + addressZipCode: string; + phoneNumber: string; +}; + type WalletAdditionalDetails = { /** Questions returned by Idology */ questions?: WalletAdditionalQuestionDetails[]; @@ -21,16 +33,10 @@ type WalletAdditionalDetails = { additionalErrorMessage?: string; isLoading?: boolean; errors?: OnyxCommon.Errors; - firstName: string; - lastName: string; - dob: string; - ssnLast4: string; - addressStreet: string; - addressCity: string; - addressState: string; - addressZipCode: string; - phoneNumber: string; }; +// TODO: refactor into one type after removing old wallet flow +type WalletAdditionalDetailsRefactor = WalletAdditionalDetails & WalletPersonalDetails; + export default WalletAdditionalDetails; -export type {WalletAdditionalQuestionDetails}; +export type {WalletAdditionalQuestionDetails, WalletPersonalDetails, WalletAdditionalDetailsRefactor}; From c888b6fbcb9a63fb21f513a7c63e6d50191f9971 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 17 Apr 2024 17:55:59 +0200 Subject: [PATCH 195/580] fix: change step order --- src/CONST.ts | 6 +++--- .../PersonalInfo/PersonalInfo.tsx | 2 +- .../PersonalInfo/substeps/Confirmation.tsx | 21 +++++++++---------- .../utils/getInitialSubstepForPersonalInfo.ts | 6 +++--- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 9e39b86aaef2..7036f69d9096 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1317,9 +1317,9 @@ const CONST = { PERSONAL_INFO: { LEGAL_NAME: 0, DATE_OF_BIRTH: 1, - PHONE_NUMBER: 2, - SSN: 3, - ADDRESS: 4, + ADDRESS: 2, + PHONE_NUMBER: 3, + SSN: 4, }, }, TIER_NAME: { diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 61957ee2865b..6c38ad83b163 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -39,7 +39,7 @@ type PersonalInfoPageOnyxProps = { type PersonalInfoPageProps = PersonalInfoPageOnyxProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; -const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, PhoneNumber, SocialSecurityNumber, Address, Confirmation]; +const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, Address, PhoneNumber, SocialSecurityNumber, Confirmation]; function PersonalInfoPage({walletAdditionalDetails, walletAdditionalDetailsDraft}: PersonalInfoPageProps) { const {translate} = useLocalize(); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx index f97e8035775a..06a214fd528e 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx @@ -67,6 +67,16 @@ function Confirmation({walletAdditionalDetails, walletAdditionalDetailsDraft, on onMove(PERSONAL_INFO_STEP_INDEXES.DATE_OF_BIRTH); }} /> + <MenuItemWithTopDescription + description={translate('personalInfoStep.address')} + title={`${values[PERSONAL_INFO_STEP_KEYS.STREET]}, ${values[PERSONAL_INFO_STEP_KEYS.CITY]}, ${values[PERSONAL_INFO_STEP_KEYS.STATE]} ${ + values[PERSONAL_INFO_STEP_KEYS.ZIP_CODE] + }`} + shouldShowRightIcon + onPress={() => { + onMove(PERSONAL_INFO_STEP_INDEXES.ADDRESS); + }} + /> <MenuItemWithTopDescription description={translate('common.phoneNumber')} title={values[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER]} @@ -83,17 +93,6 @@ function Confirmation({walletAdditionalDetails, walletAdditionalDetailsDraft, on onMove(PERSONAL_INFO_STEP_INDEXES.SSN); }} /> - <MenuItemWithTopDescription - description={translate('personalInfoStep.address')} - title={`${values[PERSONAL_INFO_STEP_KEYS.STREET]}, ${values[PERSONAL_INFO_STEP_KEYS.CITY]}, ${values[PERSONAL_INFO_STEP_KEYS.STATE]} ${ - values[PERSONAL_INFO_STEP_KEYS.ZIP_CODE] - }`} - shouldShowRightIcon - onPress={() => { - onMove(PERSONAL_INFO_STEP_INDEXES.ADDRESS); - }} - /> - <Text style={[styles.mt3, styles.ph5, styles.textMicroSupporting]}> {`${translate('personalInfoStep.byAddingThisBankAccount')} `} <TextLink diff --git a/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts b/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts index 74da50570795..f1c168be8b60 100644 --- a/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts +++ b/src/pages/EnablePayments/utils/getInitialSubstepForPersonalInfo.ts @@ -15,15 +15,15 @@ function getInitialSubstepForPersonalInfo(data: PersonalInfoStepProps): number { return 1; } - if (data[personalInfoKeys.PHONE_NUMBER] === '') { + if (data[personalInfoKeys.STREET] === '' || data[personalInfoKeys.CITY] === '' || data[personalInfoKeys.STATE] === '' || data[personalInfoKeys.ZIP_CODE] === '') { return 2; } - if (data[personalInfoKeys.SSN_LAST_4] === '') { + if (data[personalInfoKeys.PHONE_NUMBER] === '') { return 3; } - if (data[personalInfoKeys.STREET] === '' || data[personalInfoKeys.CITY] === '' || data[personalInfoKeys.STATE] === '' || data[personalInfoKeys.ZIP_CODE] === '') { + if (data[personalInfoKeys.SSN_LAST_4] === '') { return 4; } From 744c276e5122dc7044d820e326cebac4695fd1de Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 17 Apr 2024 18:00:27 +0200 Subject: [PATCH 196/580] fix: rename components --- .../EnablePayments/PersonalInfo/PersonalInfo.tsx | 12 ++++++------ .../substeps/{Address.tsx => AddressStep.tsx} | 6 +++--- .../{Confirmation.tsx => ConfirmationStep.tsx} | 6 +++--- .../{DateOfBirth.tsx => DateOfBirthStep.tsx} | 6 +++--- .../substeps/{FullName.tsx => FullNameStep.tsx} | 6 +++--- .../{PhoneNumber.tsx => PhoneNumberStep.tsx} | 6 +++--- ...curityNumber.tsx => SocialSecurityNumberStep.tsx} | 6 +++--- 7 files changed, 24 insertions(+), 24 deletions(-) rename src/pages/EnablePayments/PersonalInfo/substeps/{Address.tsx => AddressStep.tsx} (96%) rename src/pages/EnablePayments/PersonalInfo/substeps/{Confirmation.tsx => ConfirmationStep.tsx} (97%) rename src/pages/EnablePayments/PersonalInfo/substeps/{DateOfBirth.tsx => DateOfBirthStep.tsx} (95%) rename src/pages/EnablePayments/PersonalInfo/substeps/{FullName.tsx => FullNameStep.tsx} (96%) rename src/pages/EnablePayments/PersonalInfo/substeps/{PhoneNumber.tsx => PhoneNumberStep.tsx} (96%) rename src/pages/EnablePayments/PersonalInfo/substeps/{SocialSecurityNumber.tsx => SocialSecurityNumberStep.tsx} (95%) diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 6c38ad83b163..2233343ff992 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -12,7 +12,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; // TODO: uncomment in the next PR // import {parsePhoneNumber} from '@libs/PhoneNumber'; import Navigation from '@navigation/Navigation'; -import PhoneNumber from '@pages/EnablePayments/PersonalInfo/substeps/PhoneNumber'; import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -22,11 +21,12 @@ import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; import getInitialSubstepForPersonalInfo from '../utils/getInitialSubstepForPersonalInfo'; import getSubstepValues from '../utils/getSubstepValues'; -import Address from './substeps/Address'; -import Confirmation from './substeps/Confirmation'; -import DateOfBirth from './substeps/DateOfBirth'; -import FullName from './substeps/FullName'; -import SocialSecurityNumber from './substeps/SocialSecurityNumber'; +import Address from './substeps/AddressStep'; +import Confirmation from './substeps/ConfirmationStep'; +import DateOfBirth from './substeps/DateOfBirthStep'; +import FullName from './substeps/FullNameStep'; +import PhoneNumber from './substeps/PhoneNumberStep'; +import SocialSecurityNumber from './substeps/SocialSecurityNumberStep'; type PersonalInfoPageOnyxProps = { /** Reimbursement account from ONYX */ diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx similarity index 96% rename from src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx rename to src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx index f0e240274d3d..4d8f4ad6f235 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Address.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx @@ -48,7 +48,7 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL return errors; }; -function Address({walletAdditionalDetails, onNext, isEditing}: AddressProps) { +function AddressStep({walletAdditionalDetails, onNext, isEditing}: AddressProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -89,11 +89,11 @@ function Address({walletAdditionalDetails, onNext, isEditing}: AddressProps) { ); } -Address.displayName = 'Address'; +AddressStep.displayName = 'AddressStep'; export default withOnyx<AddressProps, AddressOnyxProps>({ // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, -})(Address); +})(AddressStep); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx similarity index 97% rename from src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx rename to src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx index 06a214fd528e..0529f2ae8981 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/Confirmation.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx @@ -34,7 +34,7 @@ type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; const PERSONAL_INFO_STEP_INDEXES = CONST.WALLET.SUBSTEP_INDEXES.PERSONAL_INFO; -function Confirmation({walletAdditionalDetails, walletAdditionalDetailsDraft, onNext, onMove}: ConfirmationProps) { +function ConfirmationStep({walletAdditionalDetails, walletAdditionalDetailsDraft, onNext, onMove}: ConfirmationProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -140,7 +140,7 @@ function Confirmation({walletAdditionalDetails, walletAdditionalDetailsDraft, on ); } -Confirmation.displayName = 'Confirmation'; +ConfirmationStep.displayName = 'ConfirmationStep'; export default withOnyx<ConfirmationProps, ConfirmationOnyxProps>({ // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS @@ -151,4 +151,4 @@ export default withOnyx<ConfirmationProps, ConfirmationOnyxProps>({ walletAdditionalDetailsDraft: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, }, -})(Confirmation); +})(ConfirmationStep); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx similarity index 95% rename from src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx rename to src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx index 8abcb2715e0d..b69bdf4d7621 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirth.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx @@ -45,7 +45,7 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); const maxDate = subYears(new Date(), CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); -function DateOfBirth({walletAdditionalDetails, onNext, isEditing}: DateOfBirthProps) { +function DateOfBirthStep({walletAdditionalDetails, onNext, isEditing}: DateOfBirthProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -82,11 +82,11 @@ function DateOfBirth({walletAdditionalDetails, onNext, isEditing}: DateOfBirthPr ); } -DateOfBirth.displayName = 'DateOfBirth'; +DateOfBirthStep.displayName = 'DateOfBirthStep'; export default withOnyx<DateOfBirthProps, DateOfBirthOnyxProps>({ // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, -})(DateOfBirth); +})(DateOfBirthStep); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx similarity index 96% rename from src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx rename to src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx index a1a1fd0f45f4..beccda648f9b 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullName.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx @@ -40,7 +40,7 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL return errors; }; -function FullName({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { +function FullNameStep({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -94,11 +94,11 @@ function FullName({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { ); } -FullName.displayName = 'FullName'; +FullNameStep.displayName = 'FullNameStep'; export default withOnyx<FullNameProps, FullNameOnyxProps>({ // @ts-expect-error: ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, -})(FullName); +})(FullNameStep); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx similarity index 96% rename from src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx rename to src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx index 095df6f573db..7767cea7a715 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx @@ -36,7 +36,7 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL } return errors; }; -function PhoneNumber({walletAdditionalDetails, onNext, isEditing}: PhoneNumberProps) { +function PhoneNumberStep({walletAdditionalDetails, onNext, isEditing}: PhoneNumberProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -80,11 +80,11 @@ function PhoneNumber({walletAdditionalDetails, onNext, isEditing}: PhoneNumberPr ); } -PhoneNumber.displayName = 'PhoneNumber'; +PhoneNumberStep.displayName = 'PhoneNumberStep'; export default withOnyx<PhoneNumberProps, PhoneNumberOnyxProps>({ // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, -})(PhoneNumber); +})(PhoneNumberStep); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx similarity index 95% rename from src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx rename to src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx index 5f9f2756927f..f0daa3d36110 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumber.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx @@ -37,7 +37,7 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL return errors; }; -function SocialSecurityNumber({walletAdditionalDetails, onNext, isEditing}: SocialSecurityNumberProps) { +function SocialSecurityNumberStep({walletAdditionalDetails, onNext, isEditing}: SocialSecurityNumberProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -81,11 +81,11 @@ function SocialSecurityNumber({walletAdditionalDetails, onNext, isEditing}: Soci ); } -SocialSecurityNumber.displayName = 'SocialSecurityNumber'; +SocialSecurityNumberStep.displayName = 'SocialSecurityNumberStep'; export default withOnyx<SocialSecurityNumberProps, SocialSecurityNumberOnyxProps>({ // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, }, -})(SocialSecurityNumber); +})(SocialSecurityNumberStep); From 042138bf69f2d3d67a291a1c0e21f0cec0867ce5 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 17 Apr 2024 20:06:11 +0200 Subject: [PATCH 197/580] fix: linter --- src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts | 2 +- src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx | 4 ++-- .../EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts index 24fcc88b505c..8674092383a2 100644 --- a/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts +++ b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts @@ -1,7 +1,7 @@ import type {FormOnyxKeys} from '@components/Form/types'; -import useStepFormSubmit from '@hooks/useStepFormSubmit'; import type {OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; +import useStepFormSubmit from './useStepFormSubmit'; import type {SubStepProps} from './useSubStep/types'; type UseWalletAdditionalDetailsStepFormSubmitParams = Pick<SubStepProps, 'onNext'> & { diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 2233343ff992..049e948e18f2 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -12,6 +12,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; // TODO: uncomment in the next PR // import {parsePhoneNumber} from '@libs/PhoneNumber'; import Navigation from '@navigation/Navigation'; +import getInitialSubstepForPersonalInfo from '@pages/EnablePayments/utils/getInitialSubstepForPersonalInfo'; +import getSubstepValues from '@pages/EnablePayments/utils/getSubstepValues'; import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -19,8 +21,6 @@ import ROUTES from '@src/ROUTES'; import type {WalletAdditionalDetailsForm} from '@src/types/form'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; -import getInitialSubstepForPersonalInfo from '../utils/getInitialSubstepForPersonalInfo'; -import getSubstepValues from '../utils/getSubstepValues'; import Address from './substeps/AddressStep'; import Confirmation from './substeps/ConfirmationStep'; import DateOfBirth from './substeps/DateOfBirthStep'; diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx index 0529f2ae8981..2a2809d84e10 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx @@ -14,12 +14,12 @@ import useNetwork from '@hooks/useNetwork'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; +import getSubstepValues from '@pages/EnablePayments/utils/getSubstepValues'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; import type {WalletAdditionalDetailsForm} from '@src/types/form/WalletAdditionalDetailsForm'; import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; -import getSubstepValues from '../../utils/getSubstepValues'; type ConfirmationOnyxProps = { /** wallet additional details from ONYX */ From 57b0aa82b918ee4dc8b561747f618685547217ba Mon Sep 17 00:00:00 2001 From: Sibtain Ali <allroundexperts@gmail.com> Date: Wed, 17 Apr 2024 23:33:31 +0500 Subject: [PATCH 198/580] finetune animation durations --- src/CONST.ts | 9 +++- .../config.native.ts | 5 -- src/hooks/useAnimatedHighlightStyle/config.ts | 8 --- src/hooks/useAnimatedHighlightStyle/index.ts | 52 +++++++++++++------ src/libs/actions/Policy.ts | 43 +++++---------- 5 files changed, 58 insertions(+), 59 deletions(-) delete mode 100644 src/hooks/useAnimatedHighlightStyle/config.native.ts delete mode 100644 src/hooks/useAnimatedHighlightStyle/config.ts diff --git a/src/CONST.ts b/src/CONST.ts index 74e722cdba59..f06578ef925d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -73,8 +73,13 @@ const CONST = { // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], ANDROID_PACKAGE_NAME, - ANIMATED_HIGHLIGHT_DELAY: 500, - ANIMATED_HIGHLIGHT_DURATION: 500, + WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100, + ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50, + ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300, + ANIMATED_HIGHLIGHT_START_DELAY: 10, + ANIMATED_HIGHLIGHT_START_DURATION: 300, + ANIMATED_HIGHLIGHT_END_DELAY: 800, + ANIMATED_HIGHLIGHT_END_DURATION: 2000, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATION_IN_TIMING: 100, diff --git a/src/hooks/useAnimatedHighlightStyle/config.native.ts b/src/hooks/useAnimatedHighlightStyle/config.native.ts deleted file mode 100644 index a62d3a33039e..000000000000 --- a/src/hooks/useAnimatedHighlightStyle/config.native.ts +++ /dev/null @@ -1,5 +0,0 @@ -const DELAY_FACTOR = 1.85; - -export default {}; - -export {DELAY_FACTOR}; diff --git a/src/hooks/useAnimatedHighlightStyle/config.ts b/src/hooks/useAnimatedHighlightStyle/config.ts deleted file mode 100644 index 6010c8c33aa7..000000000000 --- a/src/hooks/useAnimatedHighlightStyle/config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {isMobile} from '@libs/Browser'; - -// It takes varying amount of time to navigate to a new page on mobile and desktop -// This variable takes that into account -const DELAY_FACTOR = isMobile() ? 1 : 0.2; -export default {}; - -export {DELAY_FACTOR}; diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts index e438bd2473fa..2dbcf9b3c85a 100644 --- a/src/hooks/useAnimatedHighlightStyle/index.ts +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -3,7 +3,6 @@ import {InteractionManager} from 'react-native'; import {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; import CONST from '@src/CONST'; -import {DELAY_FACTOR} from './config'; type Props = { /** Border radius of the wrapper */ @@ -12,14 +11,26 @@ type Props = { /** Height of the item that is to be faded */ height: number; - /** Whether the item should be highlighted */ - shouldHighlight: boolean; + /** Delay before the highlighted item enters */ + itemEnterDelay?: number; + + /** Duration in which the item enters */ + itemEnterDuration?: number; + + /** Delay before the item starts to get highlighted */ + highlightStartDelay?: number; + + /** Duration in which the item gets fully highlighted */ + highlightStartDuration?: number; - /** Duration of the highlight animation */ - highlightDuration?: number; + /** Delay before the item starts to get un-highlighted */ + highlightEndDelay?: number; - /** Delay before the highlight animation starts */ - delay?: number; + /** Duration in which the item gets fully un-highlighted */ + highlightEndDuration?: number; + + /** Whether the item should be highlighted */ + shouldHighlight: boolean; }; /** @@ -28,11 +39,14 @@ type Props = { export default function useAnimatedHighlightStyle({ borderRadius, shouldHighlight, - highlightDuration = CONST.ANIMATED_HIGHLIGHT_DURATION, - delay = CONST.ANIMATED_HIGHLIGHT_DELAY, + itemEnterDelay = CONST.ANIMATED_HIGHLIGHT_ENTRY_DELAY, + itemEnterDuration = CONST.ANIMATED_HIGHLIGHT_ENTRY_DURATION, + highlightStartDelay = CONST.ANIMATED_HIGHLIGHT_START_DELAY, + highlightStartDuration = CONST.ANIMATED_HIGHLIGHT_START_DURATION, + highlightEndDelay = CONST.ANIMATED_HIGHLIGHT_END_DELAY, + highlightEndDuration = CONST.ANIMATED_HIGHLIGHT_END_DURATION, height, }: Props) { - const actualDelay = delay * DELAY_FACTOR; const repeatableProgress = useSharedValue(0); const nonRepeatableProgress = useSharedValue(shouldHighlight ? 0 : 1); const theme = useTheme(); @@ -51,14 +65,22 @@ export default function useAnimatedHighlightStyle({ InteractionManager.runAfterInteractions(() => { runOnJS(() => { - nonRepeatableProgress.value = withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})); - repeatableProgress.value = withSequence( - withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})), - withDelay(actualDelay, withTiming(0, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})), + nonRepeatableProgress.value = withDelay( + itemEnterDelay, + withTiming(1, {duration: itemEnterDuration, easing: Easing.inOut(Easing.ease)}, (finished) => { + if (!finished) { + return; + } + + repeatableProgress.value = withSequence( + withDelay(highlightStartDelay, withTiming(1, {duration: highlightStartDuration, easing: Easing.inOut(Easing.ease)})), + withDelay(highlightEndDelay, withTiming(0, {duration: highlightEndDuration, easing: Easing.inOut(Easing.ease)})), + ); + }), ); })(); }); - }, [shouldHighlight, highlightDuration, actualDelay, repeatableProgress, nonRepeatableProgress]); + }, [shouldHighlight, itemEnterDelay, itemEnterDuration, highlightStartDelay, highlightStartDuration, highlightEndDelay, highlightEndDuration, repeatableProgress, nonRepeatableProgress]); return highlightBackgroundStyle; } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 74965bcaca13..fb74f2e93590 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -71,7 +71,6 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import type {PolicySelector} from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type { InvitedEmailsToAccountIDs, @@ -3903,24 +3902,10 @@ function updatePolicyConnectionConfig(policyID: string, settingName: ValueOf<typ API.write(WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG, parameters, {optimisticData, failureData, successData}); } -function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { - const isNarrowLayout = getIsNarrowLayout(); - if (isNarrowLayout) { - setTimeout(() => { - Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID)); - }, 1000); - return; - } - - /** - * The app needs to set a navigation action to the microtask queue, it guarantees to execute Onyx.update first, then the navigation action. - * More details - https://github.com/Expensify/App/issues/37785#issuecomment-1989056726. - */ - new Promise<void>((resolve) => { - resolve(); - }).then(() => { - Navigation.navigate(featureRoute); - }); +function navigateWhenEnableFeature(policyID: string) { + setTimeout(() => { + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID)); + }, CONST.WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY); } function enablePolicyCategories(policyID: string, enabled: boolean) { @@ -3966,8 +3951,8 @@ function enablePolicyCategories(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, parameters, onyxData); - if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); + if (enabled && getIsNarrowLayout()) { + navigateWhenEnableFeature(policyID); } } @@ -4058,8 +4043,8 @@ function enablePolicyDistanceRates(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, parameters, onyxData); - if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)); + if (enabled && getIsNarrowLayout()) { + navigateWhenEnableFeature(policyID); } } @@ -4150,8 +4135,8 @@ function enablePolicyTags(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_TAGS, parameters, onyxData); - if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAGS.getRoute(policyID)); + if (enabled && getIsNarrowLayout()) { + navigateWhenEnableFeature(policyID); } } @@ -4263,8 +4248,8 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { } API.write(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData); - if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAXES.getRoute(policyID)); + if (enabled && getIsNarrowLayout()) { + navigateWhenEnableFeature(policyID); } } @@ -4354,8 +4339,8 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, parameters, onyxData); - if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)); + if (enabled && getIsNarrowLayout()) { + navigateWhenEnableFeature(policyID); } } From 10bb99688827a6ccbdbcc0f8833d5f461692ad94 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 17 Apr 2024 23:05:01 +0200 Subject: [PATCH 199/580] fix: minor fix --- src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 049e948e18f2..a06d779e690d 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -61,7 +61,8 @@ function PersonalInfoPage({walletAdditionalDetails, walletAdditionalDetailsDraft // }; // Attempt to set the personal details // Wallet.updatePersonalDetails(personalDetails); - Navigation.navigate(ROUTES.SETTINGS_WALLET); + Navigation.goBack(ROUTES.SETTINGS_WALLET); + Wallet.resetWalletAdditionalDetailsDraft(); }; const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(values), [values]); @@ -82,7 +83,7 @@ function PersonalInfoPage({walletAdditionalDetails, walletAdditionalDetailsDraft const handleBackButtonPress = () => { // TODO: connect to the fist step of the wallet setup if (screenIndex === 0) { - Navigation.navigate(ROUTES.SETTINGS_WALLET); + Navigation.goBack(ROUTES.SETTINGS_WALLET); Wallet.resetWalletAdditionalDetailsDraft(); return; } From dd0eb360c589858085f88a43a4c2f0b7c00cc99d Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 18 Apr 2024 07:26:52 +0700 Subject: [PATCH 200/580] Allow selecting a payer from the splits page --- src/ROUTES.ts | 5 ++ src/SCREENS.ts | 1 + ...raryForRefactorRequestConfirmationList.tsx | 80 +++++++++++------ src/languages/en.ts | 3 +- src/languages/es.ts | 1 + src/libs/API/parameters/SplitBillParams.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 6 ++ src/libs/OptionsListUtils.ts | 9 +- src/libs/actions/IOU.ts | 15 +++- .../iou/request/step/IOURequestStepAmount.tsx | 2 +- .../step/IOURequestStepConfirmation.tsx | 11 +-- .../request/step/IOURequestStepDistance.tsx | 2 +- .../step/IOURequestStepScan/index.native.tsx | 2 +- .../request/step/IOURequestStepScan/index.tsx | 2 +- .../request/step/IOURequestStepSplitPayer.tsx | 88 +++++++++++++++++++ .../step/IOURequestStepTaxAmountPage.tsx | 2 +- src/types/onyx/Transaction.ts | 3 + 19 files changed, 193 insertions(+), 42 deletions(-) create mode 100644 src/pages/iou/request/step/IOURequestStepSplitPayer.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ec2bf11957e1..f89085be723b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -380,6 +380,11 @@ const ROUTES = { getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '', action: ValueOf<typeof CONST.IOU.ACTION> = 'create') => getUrlWithBackToParam(`${action}/${iouType}/participants/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_STEP_SPLIT_PAYER: { + route: ':action/:iouType/confirmation/:transactionID/:reportID/payer', + getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, action: ValueOf<typeof CONST.IOU.ACTION> = 'create') => + `${action}/${iouType}/confirmation/${transactionID}/${reportID}/payer`, + }, MONEY_REQUEST_STEP_SCAN: { route: ':action/:iouType/scan/:transactionID/:reportID', getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '') => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 96372d5bbabb..dcc333559300 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -155,6 +155,7 @@ const SCREENS = { STEP_WAYPOINT: 'Money_Request_Step_Waypoint', STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount', STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', + STEP_SPLIT_PAYER: 'Money_Request_Step_Split_Payer', PARTICIPANTS: 'Money_Request_Participants', CURRENCY: 'Money_Request_Currency', WAYPOINT: 'Money_Request_Waypoint', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index f460b9d8e88c..a005240c7354 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -28,6 +28,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; +import * as UserUtils from '@libs/UserUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -43,7 +44,9 @@ import ConfirmedRoute from './ConfirmedRoute'; import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; import * as Expensicons from './Icon/Expensicons'; +import MenuItem from './MenuItem'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import {usePersonalDetails} from './OnyxProvider'; import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; @@ -113,7 +116,7 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & selectedParticipants: Participant[]; /** Payee of the money request with login */ - payeePersonalDetails?: OnyxTypes.PersonalDetails; + payeePersonalDetails?: OnyxEntry<OnyxTypes.PersonalDetails>; /** Can the participants be modified or not */ canModifyParticipants?: boolean; @@ -214,6 +217,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const personalDetails = usePersonalDetails(); const {canUseViolations} = usePermissions(); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; @@ -347,9 +351,15 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ * Returns the participants with amount */ const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]) => { - const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); + (participantsList: Participant[], payerAccountID: number) => { + const amount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? ''); + const payerAmount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? '', true); + return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( + participantsList, + amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '', + payerAmount > 0 ? CurrencyUtils.convertToDisplayString(payerAmount, iouCurrencyCode) : '', + payerAccountID, + ); }, [iouAmount, iouCurrencyCode], ); @@ -384,17 +394,25 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const selectedParticipants = useMemo(() => pickedParticipants.filter((participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails ?? currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); + const payeeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs([personalDetailsOfPayee.accountID], personalDetails), false); + const payeeIcons = [ + { + source: UserUtils.getAvatar(personalDetailsOfPayee.avatar, personalDetailsOfPayee.accountID), + name: personalDetailsOfPayee.login ?? '', + type: CONST.ICON_TYPE_AVATAR, + id: personalDetailsOfPayee.accountID, + }, + ]; const userCanModifyParticipants = useRef(!isReadOnly && canModifyParticipants && hasMultipleParticipants); useEffect(() => { userCanModifyParticipants.current = !isReadOnly && canModifyParticipants && hasMultipleParticipants; }, [isReadOnly, canModifyParticipants, hasMultipleParticipants]); - const shouldDisablePaidBySection = userCanModifyParticipants.current; const optionSelectorSections = useMemo(() => { const sections = []; const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants, personalDetailsOfPayee.accountID); let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; if (!canModifyParticipants) { @@ -402,27 +420,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ...participant, isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), })); + } else { + formattedParticipantsList = formattedParticipantsList.map((participant) => ({ + ...participant, + isDisabled: participant.accountID === personalDetailsOfPayee.accountID, + })); } - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - personalDetailsOfPayee, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - isDisabled: shouldDisablePaidBySection, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }, - ); + sections.push({ + title: translate('moneyRequestConfirmationList.splitAmounts'), + data: formattedParticipantsList, + shouldShow: true, + }); } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, @@ -439,12 +448,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ selectedParticipants, pickedParticipants, hasMultipleParticipants, - iouAmount, - iouCurrencyCode, getParticipantsWithAmount, personalDetailsOfPayee, translate, - shouldDisablePaidBySection, canModifyParticipants, ]); @@ -646,6 +652,24 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ + { + item: ( + <MenuItem + label={translate('moneyRequestConfirmationList.paidBy')} + interactive={!transaction?.isFromGlobalCreate} + description={personalDetailsOfPayee.login} + title={personalDetailsOfPayee.displayName} + icon={payeeIcons} + onPress={() => { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(iouType, transaction?.transactionID ?? '', reportID)); + }} + shouldShowRightIcon + titleWithTooltips={payeeTooltipDetails} + /> + ), + shouldShow: isTypeSplit, + isSupplementary: false, + }, { item: ( <MenuItemWithTopDescription diff --git a/src/languages/en.ts b/src/languages/en.ts index 7c379dec347d..442d95050675 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -392,6 +392,7 @@ export default { moneyRequestConfirmationList: { paidBy: 'Paid by', splitWith: 'Split with', + splitAmounts: 'Split amounts', whatsItFor: "What's it for?", }, optionsSelector: { @@ -2326,7 +2327,7 @@ export default { roomMembersPage: { memberNotFound: 'Member not found. To invite a new member to the room, please use the Invite button above.', notAuthorized: `You do not have access to this page. Are you trying to join the room? Please reach out to a member of this room so they can add you as a member! Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`, - removeMembersPrompt: 'Are you sure you want to remove the selected members from the room?', + removeMembersPrompt: 'Are you sure you want to remove the selecmoneyRequestConfirmationListted members from the room?', }, newTaskPage: { assignTask: 'Assign task', diff --git a/src/languages/es.ts b/src/languages/es.ts index 14827cb5c6b2..1e5b005db806 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -383,6 +383,7 @@ export default { moneyRequestConfirmationList: { paidBy: 'Pagado por', splitWith: 'Dividir con', + splitAmounts: 'Dividir importes', whatsItFor: '¿Para qué es?', }, optionsSelector: { diff --git a/src/libs/API/parameters/SplitBillParams.ts b/src/libs/API/parameters/SplitBillParams.ts index 310923093d5e..028e582a97e3 100644 --- a/src/libs/API/parameters/SplitBillParams.ts +++ b/src/libs/API/parameters/SplitBillParams.ts @@ -14,6 +14,7 @@ type SplitBillParams = { createdReportActionID?: string; policyID: string | undefined; chatType: string | undefined; + splitPayerAccoutIDs: number[]; }; export default SplitBillParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 7380bf102331..d5c2bb5cc8a6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -86,6 +86,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator<MoneyRequestNa [SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../../pages/iou/request/step/IOURequestStepScan').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_TAG]: () => require('../../../../pages/iou/request/step/IOURequestStepTag').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: () => require('../../../../pages/iou/request/step/IOURequestStepWaypoint').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: () => require('../../../../pages/iou/request/step/IOURequestStepSplitPayer').default as React.ComponentType, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.HOLD]: () => require('../../../../pages/iou/HoldReasonPage').default as React.ComponentType, [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 6165ccb16fa3..68b450109d1a 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -579,6 +579,7 @@ const config: LinkingOptions<RootStackParamList>['config'] = { [SCREENS.MONEY_REQUEST.PARTICIPANTS]: ROUTES.MONEY_REQUEST_PARTICIPANTS.route, [SCREENS.MONEY_REQUEST.RECEIPT]: ROUTES.MONEY_REQUEST_RECEIPT.route, [SCREENS.MONEY_REQUEST.STATE_SELECTOR]: {path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route, exact: true}, + [SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.route, [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS, [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT, [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8273278f971e..4af49d4a10bf 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -432,6 +432,12 @@ type MoneyRequestNavigatorParamList = { reportID: string; backTo: Routes; }; + [SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: { + action: ValueOf<typeof CONST.IOU.ACTION>; + iouType: ValueOf<typeof CONST.IOU.TYPE>; + transactionID: string; + reportID: string; + }; [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: undefined; [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: undefined; [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: undefined; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index aa16d7b2dc5a..438964f9659b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1962,10 +1962,15 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person /** * Build the IOUConfirmationOptions for showing participants */ -function getIOUConfirmationOptionsFromParticipants(participants: Array<Participant | ReportUtils.OptionData>, amountText: string): Array<Participant | ReportUtils.OptionData> { +function getIOUConfirmationOptionsFromParticipants( + participants: Array<Participant | ReportUtils.OptionData>, + amountText: string, + payerAmountText = '', + payerAccountID = -1, +): Array<Participant | ReportUtils.OptionData> { return participants.map((participant) => ({ ...participant, - descriptiveText: amountText, + descriptiveText: participant.accountID === payerAccountID ? payerAmountText : amountText, })); } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 896b88988818..dcde22604b18 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -442,6 +442,10 @@ function setMoneyRequestParticipants_temporaryForRefactor(transactionID: string, Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants}); } +function setSplitPayer(transactionID: string, payerAccountID: number) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {splitPayerAccountIDs: [payerAccountID]}); +} + function setMoneyRequestReceipt(transactionID: string, source: string, filename: string, isDraft: boolean, type?: string) { Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { receipt: {source, type: type ?? ''}, @@ -3506,6 +3510,7 @@ type SplitBillActionsParams = { billable?: boolean; iouRequestType?: IOURequestType; existingSplitChatReportID?: string; + splitPayerAccoutIDs?: number[]; }; /** @@ -3526,6 +3531,7 @@ function splitBill({ billable = false, iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, existingSplitChatReportID = '', + splitPayerAccoutIDs = [], }: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( @@ -3560,6 +3566,7 @@ function splitBill({ createdReportActionID: splitData.createdReportActionID, policyID: splitData.policyID, chatType: splitData.chatType, + splitPayerAccoutIDs, }; API.write(WRITE_COMMANDS.SPLIT_BILL, parameters, onyxData); @@ -3619,6 +3626,7 @@ function splitBillAndOpenReport({ createdReportActionID: splitData.createdReportActionID, policyID: splitData.policyID, chatType: splitData.chatType, + splitPayerAccoutIDs: [], }; API.write(WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT, parameters, onyxData); @@ -5800,7 +5808,7 @@ function replaceReceipt(transactionID: string, file: File, source: string) { * @param transactionID of the transaction to set the participants of * @param report attached to the transaction */ -function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry<OnyxTypes.Report>) { +function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry<OnyxTypes.Report>, iouType: ValueOf<typeof CONST.IOU.TYPE>) { // If the report is iou or expense report, we should get the chat report to set participant for request money const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report?.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; @@ -5810,6 +5818,10 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On ? [{accountID: 0, reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + if (iouType === CONST.IOU.TYPE.SPLIT) { + participants.push({accountID: currentUserAccountID, selected: true}); + } + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); } @@ -6029,6 +6041,7 @@ export { setMoneyRequestPendingFields, setMoneyRequestReceipt, setMoneyRequestAmount, + setSplitPayer, setMoneyRequestBillable, setMoneyRequestCategory, setMoneyRequestCurrency, diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 690a7e3d9d0d..92828b59952a 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -118,7 +118,7 @@ function IOURequestStepAmount({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report?.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 46e663956e4a..999302c69945 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -78,6 +78,7 @@ function IOURequestStepConfirmation({ const isSharingTrackExpense = action === CONST.IOU.ACTION.SHARE; const isCategorizingTrackExpense = action === CONST.IOU.ACTION.CATEGORIZE; const isRequestingFromTrackExpense = action === CONST.IOU.ACTION.MOVE; + const payeePersonalDetails = personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1]; const requestType = TransactionUtils.getRequestType(transaction); @@ -103,14 +104,12 @@ function IOURequestStepConfirmation({ return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); }, [iouType, transaction, translate, isSharingTrackExpense, isCategorizingTrackExpense, isRequestingFromTrackExpense]); - const participants = useMemo( - () => + const participants = useMemo(() => ( transaction?.participants?.map((participant) => { const participantAccountID = participant.accountID ?? 0; return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); - }) ?? [], - [transaction?.participants, personalDetails], - ); + }) ?? [] + ), [transaction?.participants, personalDetails]); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); const formHasBeenSubmitted = useRef(false); @@ -326,6 +325,7 @@ function IOURequestStepConfirmation({ existingSplitChatReportID: report?.reportID, billable: transaction.billable, iouRequestType: transaction.iouRequestType, + splitPayerAccoutIDs: transaction.splitPayerAccountIDs ?? [], }); } return; @@ -535,6 +535,7 @@ function IOURequestStepConfirmation({ isDistanceRequest={requestType === CONST.IOU.REQUEST_TYPE.DISTANCE} shouldShowSmartScanFields={IOUUtils.isMovingTransactionFromTrackExpense(action) ? transaction?.amount !== 0 : requestType !== CONST.IOU.REQUEST_TYPE.SCAN} action={action} + payeePersonalDetails={payeePersonalDetails} /> </View> )} diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index fae07ad2249c..b720e2f4c8f8 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -160,7 +160,7 @@ function IOURequestStepDistance({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report?.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index c1b360f89e48..50bd506fd0e0 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -175,7 +175,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, transaction?.isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 654f9e9d9f91..4037a6cffd25 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -213,7 +213,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, transaction?.isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx new file mode 100644 index 000000000000..8a8e636975cd --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx @@ -0,0 +1,88 @@ +import React, {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {usePersonalDetails} from '@components/OnyxProvider'; +import SelectionList from '@components/SelectionList'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useLocalize from '@hooks/useLocalize'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; +import * as IOUUtils from '@libs/IOUUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import type {OptionData} from '@libs/ReportUtils'; +import * as IOU from '@userActions/IOU'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; +import StepScreenWrapper from './StepScreenWrapper'; +import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; + +type IOURequestStepSplitPayerProps = WithWritableReportOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT> & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry<OnyxTypes.Transaction>; + }; + +function IOURequestStepSplitPayer({ + route: { + params: {iouType, transactionID}, + }, + transaction, +}: IOURequestStepSplitPayerProps) { + const {translate} = useLocalize(); + const personalDetails = usePersonalDetails(); + const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); + const sections = useMemo(() => { + const participantOptions = + transaction?.participants + ?.filter((participant) => Boolean(participant.accountID)) + ?.map((participant) => { + const participantAccountID = participant.accountID ?? 0; + return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); + }) ?? []; + return [ + { + title: '', + data: participantOptions.map((participantOption) => ({ + ...participantOption, + isSelected: !!transaction?.splitPayerAccountIDs && transaction?.splitPayerAccountIDs?.includes(participantOption.accountID ?? 0), + })), + }, + ]; + }, [transaction?.participants, personalDetails, transaction?.splitPayerAccountIDs]); + + const navigateBack = () => { + Navigation.goBack(); + }; + + const setSplitPayer = (item: Participant | OptionData) => { + IOU.setSplitPayer(transactionID, item.accountID ?? 0); + navigateBack(); + }; + + return ( + <StepScreenWrapper + headerTitle={translate('moneyRequestConfirmationList.paidBy')} + onBackButtonPress={navigateBack} + shouldShowNotFoundPage={!IOUUtils.isValidMoneyRequestType(iouType)} + shouldShowWrapper + testID={IOURequestStepSplitPayer.displayName} + > + <SelectionList + sections={sections} + ListItem={UserListItem} + onSelectRow={setSplitPayer} + showLoadingPlaceholder={!didScreenTransitionEnd} + /> + </StepScreenWrapper> + ); +} + +IOURequestStepSplitPayer.displayName = 'IOURequestStepSplitPayer'; + +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepSplitPayerWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepSplitPayer); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepSplitPayerWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepSplitPayerWithWritableReportOrNotFound); + +export default IOURequestStepSplitPayerWithFullTransactionOrNotFound; diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx index 40e3e5a06991..6ad4470b8e30 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.tsx @@ -128,7 +128,7 @@ function IOURequestStepTaxAmountPage({ // to the confirm step. if (report?.reportID) { // TODO: Is this really needed at all? - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); return; } diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index cf997d703680..c94f339b294c 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -225,6 +225,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< /** The linked report id for the tracked expense */ linkedTrackedExpenseReportID?: string; + + /** The payers of split bill transaction */ + splitPayerAccountIDs?: number[]; }, keyof Comment >; From c8771068fc49be5de12921021fbb5d85101960b6 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 18 Apr 2024 07:51:08 +0700 Subject: [PATCH 201/580] refactor logic display amount --- ...neyTemporaryForRefactorRequestConfirmationList.tsx | 11 +++++------ src/libs/OptionsListUtils.ts | 5 ++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index df13dcbbf46c..53ac296b54e9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -351,14 +351,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ * Returns the participants with amount */ const getParticipantsWithAmount = useCallback( - (participantsList: Participant[], payerAccountID: number) => { + (participantsList: Participant[]) => { const amount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? ''); - const payerAmount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? '', true); + const myAmount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? '', true); return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '', - payerAmount > 0 ? CurrencyUtils.convertToDisplayString(payerAmount, iouCurrencyCode) : '', - payerAccountID, + myAmount > 0 ? CurrencyUtils.convertToDisplayString(myAmount, iouCurrencyCode) : '', ); }, [iouAmount, iouCurrencyCode], @@ -412,7 +411,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const sections = []; const unselectedParticipants = pickedParticipants.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants, personalDetailsOfPayee.accountID); + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; if (!canModifyParticipants) { @@ -573,7 +572,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ playSound(SOUNDS.DONE); setDidConfirm(true); - onConfirm?.(selectedParticipants); + onConfirm?.(selectedParticipants.filter((participant) => participant.accountID !== currentUserPersonalDetails.accountID)); } }, [ diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index f21f21ddf36f..47ccf9927585 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1978,12 +1978,11 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person function getIOUConfirmationOptionsFromParticipants( participants: Array<Participant | ReportUtils.OptionData>, amountText: string, - payerAmountText = '', - payerAccountID = -1, + myAmountText = '', ): Array<Participant | ReportUtils.OptionData> { return participants.map((participant) => ({ ...participant, - descriptiveText: participant.accountID === payerAccountID ? payerAmountText : amountText, + descriptiveText: participant.accountID === currentUserAccountID ? myAmountText : amountText, })); } From 3c4f3396b327decc1ef071172ffee86011bdc457 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 18 Apr 2024 12:03:56 +0700 Subject: [PATCH 202/580] run prettier --- ...eyTemporaryForRefactorRequestConfirmationList.tsx | 11 ++--------- .../iou/request/step/IOURequestStepConfirmation.tsx | 8 +++++--- .../iou/request/step/IOURequestStepSplitPayer.tsx | 12 ++++++------ 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 53ac296b54e9..adfe338c18a5 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -443,15 +443,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [ - selectedParticipants, - pickedParticipants, - hasMultipleParticipants, - getParticipantsWithAmount, - personalDetailsOfPayee, - translate, - canModifyParticipants, - ]); + }, [selectedParticipants, pickedParticipants, hasMultipleParticipants, getParticipantsWithAmount, personalDetailsOfPayee, translate, canModifyParticipants]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -589,6 +581,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ iouAmount, isEditingSplitBill, onConfirm, + currentUserPersonalDetails.accountID, ], ); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index dce7f4319f6f..237ad5eb7eb2 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -104,12 +104,14 @@ function IOURequestStepConfirmation({ return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); }, [iouType, report, transaction, translate, isSharingTrackExpense, isCategorizingTrackExpense, isRequestingFromTrackExpense]); - const participants = useMemo(() => ( + const participants = useMemo( + () => transaction?.participants?.map((participant) => { const participantAccountID = participant.accountID ?? 0; return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); - }) ?? [] - ), [transaction?.participants, personalDetails]); + }) ?? [], + [transaction?.participants, personalDetails], + ); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); const formHasBeenSubmitted = useRef(false); diff --git a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx index 8a8e636975cd..8327e37579a9 100644 --- a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx +++ b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx @@ -19,9 +19,9 @@ import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotF import withWritableReportOrNotFound from './withWritableReportOrNotFound'; type IOURequestStepSplitPayerProps = WithWritableReportOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT> & { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - transaction: OnyxEntry<OnyxTypes.Transaction>; - }; + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry<OnyxTypes.Transaction>; +}; function IOURequestStepSplitPayer({ route: { @@ -44,9 +44,9 @@ function IOURequestStepSplitPayer({ { title: '', data: participantOptions.map((participantOption) => ({ - ...participantOption, - isSelected: !!transaction?.splitPayerAccountIDs && transaction?.splitPayerAccountIDs?.includes(participantOption.accountID ?? 0), - })), + ...participantOption, + isSelected: !!transaction?.splitPayerAccountIDs && transaction?.splitPayerAccountIDs?.includes(participantOption.accountID ?? 0), + })), }, ]; }, [transaction?.participants, personalDetails, transaction?.splitPayerAccountIDs]); From 535b2838ef4e5ed9727d540f911ba8397976ca75 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 18 Apr 2024 12:25:33 +0700 Subject: [PATCH 203/580] Not allow deselect the payer --- src/ROUTES.ts | 2 +- ...yTemporaryForRefactorRequestConfirmationList.tsx | 13 ++++--------- src/libs/actions/IOU.ts | 1 + 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index e894e470b670..89044261145d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -383,7 +383,7 @@ const ROUTES = { MONEY_REQUEST_STEP_SPLIT_PAYER: { route: ':action/:iouType/confirmation/:transactionID/:reportID/payer', getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, action: ValueOf<typeof CONST.IOU.ACTION> = 'create') => - `${action}/${iouType}/confirmation/${transactionID}/${reportID}/payer`, + `${action}/${iouType}/confirmation/${transactionID}/${reportID}/payer` as const, }, MONEY_REQUEST_STEP_SCAN: { route: ':action/:iouType/scan/:transactionID/:reportID', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index adfe338c18a5..ad2a2d63ad2e 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -419,11 +419,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ...participant, isDisabled: ReportUtils.isOptimisticPersonalDetail(participant.accountID ?? -1), })); - } else { - formattedParticipantsList = formattedParticipantsList.map((participant) => ({ - ...participant, - isDisabled: participant.accountID === personalDetailsOfPayee.accountID, - })); } sections.push({ @@ -449,8 +444,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (!hasMultipleParticipants) { return []; } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetailsOfPayee)]; - }, [selectedParticipants, hasMultipleParticipants, personalDetailsOfPayee]); + return [...selectedParticipants]; + }, [selectedParticipants, hasMultipleParticipants]); useEffect(() => { if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { @@ -500,12 +495,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const selectParticipant = useCallback( (option: Participant) => { // Return early if selected option is currently logged in user. - if (option.accountID === session?.accountID) { + if (option.accountID === session?.accountID || option.accountID === personalDetailsOfPayee.accountID) { return; } onSelectParticipant?.(option); }, - [session?.accountID, onSelectParticipant], + [session?.accountID, onSelectParticipant, personalDetailsOfPayee.accountID], ); /** diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5d9196fc0b10..8eb5e56fc4d1 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -335,6 +335,7 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry<OnyxTypes.Policy>, transactionID: newTransactionID, isFromGlobalCreate, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + splitPayerAccountIDs: [currentUserPersonalDetails.accountID] }); } From 24fb63b19a7b0bf01fedc02146fdc29a9d2d006f Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 18 Apr 2024 12:49:23 +0700 Subject: [PATCH 204/580] update participants to include the payee --- ...eyTemporaryForRefactorRequestConfirmationList.tsx | 6 +++--- src/libs/actions/IOU.ts | 2 +- .../iou/request/step/IOURequestStepConfirmation.tsx | 12 ++++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index ad2a2d63ad2e..3a373dca77f2 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -438,7 +438,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }); } return sections; - }, [selectedParticipants, pickedParticipants, hasMultipleParticipants, getParticipantsWithAmount, personalDetailsOfPayee, translate, canModifyParticipants]); + }, [selectedParticipants, pickedParticipants, hasMultipleParticipants, getParticipantsWithAmount, translate, canModifyParticipants]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -586,7 +586,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0; + const shouldDisableButton = isTypeSplit ? selectedParticipants.length === 1 : selectParticipants.length === 0; const button = shouldShowSettlementButton ? ( <SettlementButton @@ -634,7 +634,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ {button} </> ); - }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2, isTypeSplit]); // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8eb5e56fc4d1..f870ef0ddc97 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -335,7 +335,7 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry<OnyxTypes.Policy>, transactionID: newTransactionID, isFromGlobalCreate, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, - splitPayerAccountIDs: [currentUserPersonalDetails.accountID] + splitPayerAccountIDs: [currentUserPersonalDetails.accountID], }); } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 237ad5eb7eb2..bb709b7f6c00 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -115,6 +115,18 @@ function IOURequestStepConfirmation({ const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); const formHasBeenSubmitted = useRef(false); + useEffect(() => { + if (transaction?.participants?.findIndex((participant) => participant.accountID === payeePersonalDetails?.accountID) !== -1 || iouType !== CONST.IOU.TYPE.SPLIT) { + return; + } + + const payeeParticipant = OptionsListUtils.getParticipantsOption({accountID: payeePersonalDetails?.accountID, selected: true}, personalDetails); + IOU.setMoneyRequestParticipants_temporaryForRefactor(transaction.transactionID, [...(transaction?.participants ?? []), payeeParticipant]); + + // We only want to run it when the component is mounted + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { const policyExpenseChat = participants?.find((participant) => participant.isPolicyExpenseChat); if (policyExpenseChat?.policyID) { From 0de971c1fbea30434cf9b7ef6f73365b473a559c Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 18 Apr 2024 12:53:36 +0700 Subject: [PATCH 205/580] fix lint --- .../MoneyTemporaryForRefactorRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 3a373dca77f2..3246928ec271 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -586,7 +586,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = isTypeSplit ? selectedParticipants.length === 1 : selectParticipants.length === 0; + const shouldDisableButton = isTypeSplit ? selectedParticipants.length === 1 : selectedParticipants.length === 0; const button = shouldShowSettlementButton ? ( <SettlementButton From ae06c5bb198db6d3c9ca8056e32f283c3814210d Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Thu, 18 Apr 2024 09:12:03 +0200 Subject: [PATCH 206/580] Add optimistic personal details for the receiver; update params --- src/libs/API/parameters/SendInvoiceParams.ts | 12 +-- src/libs/actions/IOU.ts | 89 ++++++++++++-------- 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts index 7b074657233e..5e65968654e6 100644 --- a/src/libs/API/parameters/SendInvoiceParams.ts +++ b/src/libs/API/parameters/SendInvoiceParams.ts @@ -8,12 +8,12 @@ type SendInvoiceParams = { merchant: string; date: string; category?: string; - optimisticInvoiceRoomID?: string; - optimisticCreatedChatReportActionID: string; - optimisticInvoiceReportID: string; - optimisticReportPreviewReportActionID: string; - optimisticTransactionID: string; - optimisticTransactionThreadReportID: string; + invoiceRoomID?: string; + createdChatReportActionID: string; + invoiceReportID: string; + reportPreviewReportActionID: string; + transactionID: string; + transactionThreadReportID: string; }; export default SendInvoiceParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index d6be557b8107..87d9c46cc142 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -851,6 +851,7 @@ function buildOnyxDataForInvoice( chatCreatedAction: OptimisticCreatedReportAction, iouCreatedAction: OptimisticCreatedReportAction, iouAction: OptimisticIOUReportAction, + optimisticPersonalDetailListAction: OnyxTypes.PersonalDetailsList, reportPreviewAction: ReportAction, optimisticPolicyRecentlyUsedCategories: string[], optimisticPolicyRecentlyUsedTags: OnyxTypes.RecentlyUsedTags, @@ -966,6 +967,14 @@ function buildOnyxDataForInvoice( }); } + if (!isEmptyObject(optimisticPersonalDetailListAction)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: optimisticPersonalDetailListAction, + }); + } + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1624,8 +1633,10 @@ function getSendInvoiceInformation( const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.policyID)?.policyID ?? ''; - const receiverAccountID = participants?.find((participant) => participant?.accountID)?.accountID ?? -1; - const receiver = ReportUtils.getPersonalDetailsForAccountID(receiverAccountID); + const receiverParticipant = participants?.find((participant) => participant?.accountID); + const receiverAccountID = receiverParticipant?.accountID ?? -1; + let receiver = ReportUtils.getPersonalDetailsForAccountID(receiverAccountID); + let optimisticPersonalDetailListAction = {}; // STEP 1: Get existing chat report OR build a new optimistic one let isNewChatReport = false; @@ -1640,10 +1651,10 @@ function getSendInvoiceInformation( chatReport = ReportUtils.buildOptimisticChatReport([receiverAccountID, currentUserAccountID], CONST.REPORT.DEFAULT_REPORT_NAME, CONST.REPORT.CHAT_TYPE.INVOICE, senderWorkspaceID); } - // STEP 3: Create a new optimistic invoice report. + // STEP 2: Create a new optimistic invoice report. const optimisticInvoiceReport = ReportUtils.buildOptimisticInvoiceReport(chatReport.reportID, senderWorkspaceID, receiverAccountID, receiver.displayName ?? '', amount, currency); - // STEP 2: Build optimistic receipt and transaction + // STEP 3: Build optimistic receipt and transaction const receiptObject: Receipt = {}; let filename; if (receipt?.source) { @@ -1671,7 +1682,21 @@ function getSendInvoiceInformation( const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(optimisticInvoiceReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(optimisticInvoiceReport.policyID, tag); - // STEP 4: Build optimistic reportActions. + // STEP 4: Add optimistic personal details for participant + const shouldCreateOptimisticPersonalDetails = isNewChatReport && !allPersonalDetails[receiverAccountID]; + if (shouldCreateOptimisticPersonalDetails) { + receiver = { + accountID: receiverAccountID, + avatar: UserUtils.getDefaultAvatarURL(receiverAccountID), + displayName: LocalePhoneNumber.formatPhoneNumber(receiverParticipant?.login ?? ''), + login: receiverParticipant?.login, + isOptimisticPersonalDetail: true, + }; + + optimisticPersonalDetailListAction = {[receiverAccountID]: receiver}; + } + + // STEP 5: Build optimistic reportActions. let inviteReportAction: OptimisticInviteReportAction | undefined; const [optimisticCreatedActionForChat, optimisticCreatedActionForIOUReport, iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread] = ReportUtils.buildOptimisticMoneyRequestEntities( @@ -1694,7 +1719,7 @@ function getSendInvoiceInformation( } const reportPreviewAction = ReportUtils.buildOptimisticReportPreview(chatReport, optimisticInvoiceReport, trimmedComment, optimisticTransaction); - // STEP 4: Build Onyx Data + // STEP 6: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForInvoice( chatReport, optimisticInvoiceReport, @@ -1702,6 +1727,7 @@ function getSendInvoiceInformation( optimisticCreatedActionForChat, optimisticCreatedActionForIOUReport, iouAction, + optimisticPersonalDetailListAction, reportPreviewAction, optimisticPolicyRecentlyUsedCategories, optimisticPolicyRecentlyUsedTags, @@ -1717,12 +1743,12 @@ function getSendInvoiceInformation( return { senderWorkspaceID, receiver, - optimisticInvoiceRoomID: chatReport.reportID, - optimisticCreatedChatReportActionID: optimisticCreatedActionForChat.reportActionID, - optimisticInvoiceReportID: optimisticInvoiceReport.reportID, - optimisticReportPreviewReportActionID: reportPreviewAction.reportActionID, - optimisticTransactionID: optimisticTransaction.transactionID, - optimisticTransactionThreadReportID: optimisticTransactionThread.reportID, + invoiceRoomID: chatReport.reportID, + createdChatReportActionID: optimisticCreatedActionForChat.reportActionID, + invoiceReportID: optimisticInvoiceReport.reportID, + reportPreviewReportActionID: reportPreviewAction.reportActionID, + transactionID: optimisticTransaction.transactionID, + transactionThreadReportID: optimisticTransactionThread.reportID, onyxData: { optimisticData, successData, @@ -3327,17 +3353,8 @@ function sendInvoice( policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, ) { - const { - senderWorkspaceID, - receiver, - optimisticInvoiceRoomID, - optimisticCreatedChatReportActionID, - optimisticInvoiceReportID, - optimisticReportPreviewReportActionID, - optimisticTransactionID, - optimisticTransactionThreadReportID, - onyxData, - } = getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); + const {senderWorkspaceID, receiver, invoiceRoomID, createdChatReportActionID, invoiceReportID, reportPreviewReportActionID, transactionID, transactionThreadReportID, onyxData} = + getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); let parameters: SendInvoiceParams = { senderWorkspaceID, @@ -3347,33 +3364,31 @@ function sendInvoice( merchant: transaction?.merchant ?? '', category: transaction?.category, date: transaction?.created ?? '', - optimisticInvoiceRoomID, - optimisticCreatedChatReportActionID, - optimisticInvoiceReportID, - optimisticReportPreviewReportActionID, - optimisticTransactionID, - optimisticTransactionThreadReportID, + invoiceRoomID, + createdChatReportActionID, + invoiceReportID, + reportPreviewReportActionID, + transactionID, + transactionThreadReportID, }; - if (!invoiceChatReport) { + if (invoiceChatReport) { parameters = { ...parameters, - receiverEmail: receiver.login, + receiverInvoiceRoomID: invoiceChatReport.reportID, }; - } - - if (!transaction?.isFromGlobalCreate && invoiceChatReport) { + } else { parameters = { ...parameters, - receiverInvoiceRoomID: invoiceChatReport.reportID, + receiverEmail: receiver.login, }; } API.write(WRITE_COMMANDS.SEND_INVOICE, parameters, onyxData); resetMoneyRequestInfo(); - Navigation.dismissModal(optimisticInvoiceRoomID); - Report.notifyNewAction(optimisticInvoiceRoomID, receiver.accountID); + Navigation.dismissModal(invoiceRoomID); + Report.notifyNewAction(invoiceRoomID, receiver.accountID); } /** From 695720dc7ce99ef7b8af8bfbdf84220b26ebcfef Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 18 Apr 2024 14:37:35 +0700 Subject: [PATCH 207/580] revert mistake change --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 24ef3eee3215..33c9180d0f43 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2318,7 +2318,7 @@ export default { roomMembersPage: { memberNotFound: 'Member not found. To invite a new member to the room, please use the Invite button above.', notAuthorized: `You do not have access to this page. Are you trying to join the room? Please reach out to a member of this room so they can add you as a member! Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`, - removeMembersPrompt: 'Are you sure you want to remove the selecmoneyRequestConfirmationListted members from the room?', + removeMembersPrompt: 'Are you sure you want to remove the selected members from the room?', }, newTaskPage: { assignTask: 'Assign task', From 8e6e602501eb9a8d550db3dcb88bfcfff79dfb49 Mon Sep 17 00:00:00 2001 From: Cong Pham <ptcong411@gmail.com> Date: Thu, 18 Apr 2024 15:09:15 +0700 Subject: [PATCH 208/580] set onfido web full height --- src/components/Onfido/index.css | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/Onfido/index.css b/src/components/Onfido/index.css index 53f7888fc385..66fe571af9cc 100644 --- a/src/components/Onfido/index.css +++ b/src/components/Onfido/index.css @@ -56,10 +56,13 @@ height: 92% !important; } - /* - * Solves issue with height not working for `onfido-sdk-ui-Modal-inner` container when device width is in between 490 - 600pixels. - */ - #onfido-mount { - height: 100%; - } +} + +#onfido-mount { + height: 100%; +} + +#onfido-sdk { + min-height: initial !important; + max-height: initial !important; } \ No newline at end of file From f19a19cf96d7dcc0bf3e3c076dc1bf94fc55847b Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Thu, 18 Apr 2024 10:13:21 +0200 Subject: [PATCH 209/580] Add SendInvoiceInformation type --- src/libs/actions/IOU.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index fc4877c80a74..c66a8da736e4 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -95,6 +95,18 @@ type TrackExpenseInformation = { onyxData: OnyxData; }; +type SendInvoiceInformation = { + senderWorkspaceID: string; + receiver: Partial<OnyxTypes.PersonalDetails>; + invoiceRoomID: string; + createdChatReportActionID: string; + invoiceReportID: string; + reportPreviewReportActionID: string; + transactionID: string; + transactionThreadReportID: string; + onyxData: OnyxData; +}; + type SplitData = { chatReportID: string; transactionID: string; @@ -1627,7 +1639,7 @@ function getSendInvoiceInformation( policy?: OnyxEntry<OnyxTypes.Policy>, policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, -) { +): SendInvoiceInformation { const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.policyID)?.policyID ?? ''; From c386da0e430843c48c6c5c55169b2eed4e6840af Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 18 Apr 2024 13:04:03 +0200 Subject: [PATCH 210/580] fix isReadOnly condition --- src/libs/ReportUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5fc5faeb1532..2bc60df19a1a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5275,7 +5275,11 @@ function isMoneyRequestReportPendingDeletion(report: OnyxEntry<Report> | EmptyOb * Checks if report is in read-only mode. */ function isReadOnly(report: OnyxEntry<Report>): boolean { - return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE) ?? false; + if (Array.isArray(report?.permissions)) { + return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE); + } + + return false; } function canUserPerformWriteAction(report: OnyxEntry<Report>) { From 8243a2b0e7eb62cc5d5bb7ac0dff30c84ac72e00 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 18 Apr 2024 13:04:30 +0200 Subject: [PATCH 211/580] improve OnboardingReportFooterMessage --- src/pages/home/report/OnboardingReportFooterMessage.tsx | 6 ++++-- src/pages/home/report/ReportFooter.tsx | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index 54659c14cf6e..1d02a9ebeb94 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -10,13 +10,14 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import * as ReportInstance from '@userActions/Report'; +import type {OnboardingPurposeType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy as PolicyType, Report} from '@src/types/onyx'; // TODO: Use a proper choice type -type OnboardingReportFooterMessageOnyxProps = {choice: OnyxEntry<string>; reports: OnyxCollection<Report>; policies: OnyxCollection<PolicyType>}; +type OnboardingReportFooterMessageOnyxProps = {choice: OnyxEntry<OnboardingPurposeType>; reports: OnyxCollection<Report>; policies: OnyxCollection<PolicyType>}; type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps; function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingReportFooterMessageProps) { @@ -33,7 +34,7 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe const content = useMemo(() => { switch (choice) { - case CONST.INTRO_CHOICES.MANAGE_TEAM: + case CONST.ONBOARDING_CHOICES.MANAGE_TEAM: return ( <> {`${translate('onboardingBottomMessage.newDotManageTeam.phrase1')}`} @@ -68,6 +69,7 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe styles.chatFooter, isSmallScreenWidth ? styles.mb5 : styles.mb4, styles.mh5, + styles.mt4, styles.flexRow, styles.alignItemsCenter, styles.p4, diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index eec34fa06903..c36c42a3e36b 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -131,7 +131,7 @@ function ReportFooter({ ); if (isReadOnlyReport) { - <OnboardingReportFooterMessage />; + return <OnboardingReportFooterMessage />; } return ( From 3896162ff35a6e478f4562169da4f8ca0d0c9de7 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 18 Apr 2024 13:04:51 +0200 Subject: [PATCH 212/580] add permissions to report --- src/pages/home/ReportScreen.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index cdd843f65fb3..0dad3d295cff 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -206,6 +206,7 @@ function ReportScreen({ isOptimisticReport: reportProp?.isOptimisticReport, lastMentionedTime: reportProp?.lastMentionedTime, avatarUrl: reportProp?.avatarUrl, + permissions: reportProp?.permissions, }), [ reportProp?.lastReadTime, @@ -245,6 +246,7 @@ function ReportScreen({ reportProp?.isOptimisticReport, reportProp?.lastMentionedTime, reportProp?.avatarUrl, + reportProp?.permissions, ], ); From af3b05ffc6e603b708e5c3f7204373e1afa30382 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 18 Apr 2024 13:14:40 +0200 Subject: [PATCH 213/580] improve read only for tasks --- .../ReportActionItem/TaskPreview.tsx | 7 +---- src/components/ReportActionItem/TaskView.tsx | 2 +- src/libs/ReportUtils.ts | 28 +++++++++++-------- src/libs/actions/Task.ts | 4 +++ 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index b396816b024a..8e8b3b930be7 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -76,7 +76,6 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR const htmlForTaskPreview = taskAssigneeAccountID !== 0 ? `<comment><mention-user accountid="${taskAssigneeAccountID}"></mention-user> ${taskTitle}</comment>` : `<comment>${taskTitle}</comment>`; const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action); - const isReadOnly = ReportUtils.isReadOnly(taskReport); if (isDeletedParentAction) { return <RenderHTML html={`<comment>${translate('parentReportAction.deletedTask')}</comment>`} />; @@ -99,12 +98,8 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR style={[styles.mr2]} containerStyle={[styles.taskCheckbox]} isChecked={isTaskCompleted} - disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID) && !isReadOnly} + disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID)} onPress={Session.checkIfActionIsAllowed(() => { - if (isReadOnly) { - return; - } - if (isTaskCompleted) { Task.reopenTask(taskReport); } else { diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 550b9fc6b05c..9711e126907f 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -60,7 +60,7 @@ function TaskView({report, shouldShowHorizontalRule, ...props}: TaskViewProps) { const isOpen = ReportUtils.isOpenTaskReport(report); const canModifyTask = Task.canModifyTask(report, props.currentUserPersonalDetails.accountID); const disableState = !canModifyTask; - const isDisableInteractive = !canModifyTask || !isOpen || ReportUtils.isReadOnly(report); + const isDisableInteractive = !canModifyTask || !isOpen; const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const {translate} = useLocalize(); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2bc60df19a1a..0536545aabfc 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1186,10 +1186,26 @@ function isJoinRequestInAdminRoom(report: OnyxEntry<Report>): boolean { return ReportActionsUtils.isActionableJoinRequestPending(report.reportID); } +/** + * + * Checks if report is in read-only mode. + */ +function isReadOnly(report: OnyxEntry<Report>): boolean { + if (Array.isArray(report?.permissions)) { + return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE); + } + + return false; +} + /** * Checks if the current user is allowed to comment on the given report. */ function isAllowedToComment(report: OnyxEntry<Report>): boolean { + if (isReadOnly(report)) { + return false; + } + // Default to allowing all users to post const capability = report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL; @@ -5270,18 +5286,6 @@ function isMoneyRequestReportPendingDeletion(report: OnyxEntry<Report> | EmptyOb return parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } -/** - * - * Checks if report is in read-only mode. - */ -function isReadOnly(report: OnyxEntry<Report>): boolean { - if (Array.isArray(report?.permissions)) { - return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE); - } - - return false; -} - function canUserPerformWriteAction(report: OnyxEntry<Report>) { const reportErrors = getAddWorkspaceRoomOrChatReportErrors(report); diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index f74b451c29f6..c1c64a7fbf03 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1000,6 +1000,10 @@ function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID return true; } + if (ReportUtils.isReadOnly(ReportUtils.getReport(taskReport?.reportID))) { + return false; + } + return !isEmptyObject(taskReport) && ReportUtils.isAllowedToComment(taskReport); } From bffc4b6e4a6be97cac4de9003ed626b32387d161 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 18 Apr 2024 14:42:00 +0200 Subject: [PATCH 214/580] fix: add issue link --- src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx index 82bdb3e6bdc4..31c11f0c08fc 100644 --- a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx +++ b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx @@ -54,7 +54,7 @@ function AddBankAccount({personalBankAccount, plaidData, personalBankAccountDraf const exitFlow = (shouldContinue = false) => { const exitReportID = personalBankAccount?.exitReportID; - // TODO: This should be updated to the correct route once the refactor is complete + // TODO: https://github.com/Expensify/App/issues/36648 This should be updated to the correct route once the refactor is complete const onSuccessFallbackRoute = ROUTES.SETTINGS_ENABLE_PAYMENTS_REFACTOR; if (exitReportID) { From ec9ed98088e9a0a80966cacccf89901b621c4586 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 18 Apr 2024 14:58:33 +0200 Subject: [PATCH 215/580] fix: minor fix --- src/hooks/useReimbursementAccountStepFormSubmit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useReimbursementAccountStepFormSubmit.ts b/src/hooks/useReimbursementAccountStepFormSubmit.ts index bd17bfaa1234..d0d9dd07035a 100644 --- a/src/hooks/useReimbursementAccountStepFormSubmit.ts +++ b/src/hooks/useReimbursementAccountStepFormSubmit.ts @@ -1,7 +1,7 @@ import type {FormOnyxKeys} from '@components/Form/types'; -import useStepFormSubmit from '@hooks/useStepFormSubmit'; import type {OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; +import useStepFormSubmit from './useStepFormSubmit'; import type {SubStepProps} from './useSubStep/types'; type UseReimbursementAccountStepFormSubmitParams = Pick<SubStepProps, 'onNext'> & { From d6f65ef5d82339715b61c6ed17741bdd66edb7ec Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 18 Apr 2024 15:00:44 +0200 Subject: [PATCH 216/580] fix: minor fix --- src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx index 31c11f0c08fc..79c91af178c3 100644 --- a/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx +++ b/src/pages/EnablePayments/AddBankAccount/AddBankAccount.tsx @@ -55,7 +55,7 @@ function AddBankAccount({personalBankAccount, plaidData, personalBankAccountDraf const exitFlow = (shouldContinue = false) => { const exitReportID = personalBankAccount?.exitReportID; // TODO: https://github.com/Expensify/App/issues/36648 This should be updated to the correct route once the refactor is complete - const onSuccessFallbackRoute = ROUTES.SETTINGS_ENABLE_PAYMENTS_REFACTOR; + const onSuccessFallbackRoute = personalBankAccount?.onSuccessFallbackRoute ?? ''; if (exitReportID) { Navigation.dismissModal(exitReportID); From 16b206788ee46d83e881d7e2fda406efb489a9ab Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 18 Apr 2024 15:03:14 +0200 Subject: [PATCH 217/580] refactor: replace withOnyx with useOnyx --- .../PersonalInfo/PersonalInfo.tsx | 30 +++++------------- .../PersonalInfo/substeps/AddressStep.tsx | 22 +++---------- .../substeps/ConfirmationStep.tsx | 31 ++++--------------- .../PersonalInfo/substeps/DateOfBirthStep.tsx | 22 +++---------- .../PersonalInfo/substeps/FullNameStep.tsx | 22 +++---------- .../PersonalInfo/substeps/PhoneNumberStep.tsx | 22 +++---------- .../substeps/SocialSecurityNumberStep.tsx | 22 +++---------- 7 files changed, 38 insertions(+), 133 deletions(-) diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index a06d779e690d..45e3561587b3 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -1,6 +1,6 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; @@ -28,26 +28,19 @@ import FullName from './substeps/FullNameStep'; import PhoneNumber from './substeps/PhoneNumberStep'; import SocialSecurityNumber from './substeps/SocialSecurityNumberStep'; -type PersonalInfoPageOnyxProps = { - /** Reimbursement account from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; - - /** The draft values of the bank account being setup */ - walletAdditionalDetailsDraft: OnyxEntry<WalletAdditionalDetailsForm>; -}; - -type PersonalInfoPageProps = PersonalInfoPageOnyxProps; - const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, Address, PhoneNumber, SocialSecurityNumber, Confirmation]; -function PersonalInfoPage({walletAdditionalDetails, walletAdditionalDetailsDraft}: PersonalInfoPageProps) { +function PersonalInfoPage() { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); + const [walletAdditionalDetailsDraft] = useOnyx(ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT); + const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, walletAdditionalDetailsDraft, walletAdditionalDetails), [walletAdditionalDetails, walletAdditionalDetailsDraft]); const submit = () => { - // TODO: uncomment in the next PR + // TODO: uncomment in one of the next PR https://github.com/Expensify/App/issues/36648 // const personalDetails = { // phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', // legalFirstName: values?.[PERSONAL_INFO_STEP_KEYS.FIRST_NAME] ?? '', @@ -117,13 +110,4 @@ function PersonalInfoPage({walletAdditionalDetails, walletAdditionalDetailsDraft PersonalInfoPage.displayName = 'PersonalInfoPage'; -export default withOnyx<PersonalInfoPageProps, PersonalInfoPageOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM - walletAdditionalDetails: { - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, - }, - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM - walletAdditionalDetailsDraft: { - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, - }, -})(PersonalInfoPage); +export default PersonalInfoPage; diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx index 4d8f4ad6f235..7d62eff8072e 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import Text from '@components/Text'; @@ -14,14 +13,6 @@ import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields'; import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; -import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; - -type AddressOnyxProps = { - /** wallet additional details from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; -}; - -type AddressProps = AddressOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; @@ -48,10 +39,12 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL return errors; }; -function AddressStep({walletAdditionalDetails, onNext, isEditing}: AddressProps) { +function AddressStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); + const defaultValues = { street: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.STREET] ?? '', city: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.CITY] ?? '', @@ -91,9 +84,4 @@ function AddressStep({walletAdditionalDetails, onNext, isEditing}: AddressProps) AddressStep.displayName = 'AddressStep'; -export default withOnyx<AddressProps, AddressOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS - walletAdditionalDetails: { - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, - }, -})(AddressStep); +export default AddressStep; diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx index 2a2809d84e10..b24400face07 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/ConfirmationStep.tsx @@ -1,7 +1,6 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -18,27 +17,18 @@ import getSubstepValues from '@pages/EnablePayments/utils/getSubstepValues'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; -import type {WalletAdditionalDetailsForm} from '@src/types/form/WalletAdditionalDetailsForm'; -import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; - -type ConfirmationOnyxProps = { - /** wallet additional details from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; - - /** The draft values of the bank account being setup */ - walletAdditionalDetailsDraft: OnyxEntry<WalletAdditionalDetailsForm>; -}; - -type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; const PERSONAL_INFO_STEP_INDEXES = CONST.WALLET.SUBSTEP_INDEXES.PERSONAL_INFO; -function ConfirmationStep({walletAdditionalDetails, walletAdditionalDetailsDraft, onNext, onMove}: ConfirmationProps) { +function ConfirmationStep({onNext, onMove}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); + const [walletAdditionalDetailsDraft] = useOnyx(ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT); + const isLoading = walletAdditionalDetailsDraft?.isLoading ?? false; const error = ErrorUtils.getLatestErrorMessage(walletAdditionalDetails ?? {}); const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, walletAdditionalDetailsDraft, walletAdditionalDetails), [walletAdditionalDetails, walletAdditionalDetailsDraft]); @@ -142,13 +132,4 @@ function ConfirmationStep({walletAdditionalDetails, walletAdditionalDetailsDraft ConfirmationStep.displayName = 'ConfirmationStep'; -export default withOnyx<ConfirmationProps, ConfirmationOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS - walletAdditionalDetails: { - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, - }, - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS - walletAdditionalDetailsDraft: { - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, - }, -})(ConfirmationStep); +export default ConfirmationStep; diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx index b69bdf4d7621..ad59c0eb1d00 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx @@ -1,7 +1,6 @@ import {subYears} from 'date-fns'; import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -16,14 +15,6 @@ import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; -import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; - -type DateOfBirthOnyxProps = { - /** Reimbursement account from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; -}; - -type DateOfBirthProps = DateOfBirthOnyxProps & SubStepProps; const PERSONAL_INFO_DOB_KEY = INPUT_IDS.PERSONAL_INFO_STEP.DOB; const STEP_FIELDS = [PERSONAL_INFO_DOB_KEY]; @@ -45,10 +36,12 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL const minDate = subYears(new Date(), CONST.DATE_BIRTH.MAX_AGE); const maxDate = subYears(new Date(), CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); -function DateOfBirthStep({walletAdditionalDetails, onNext, isEditing}: DateOfBirthProps) { +function DateOfBirthStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); + const dobDefaultValue = walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? walletAdditionalDetails?.[PERSONAL_INFO_DOB_KEY] ?? ''; const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ @@ -84,9 +77,4 @@ function DateOfBirthStep({walletAdditionalDetails, onNext, isEditing}: DateOfBir DateOfBirthStep.displayName = 'DateOfBirthStep'; -export default withOnyx<DateOfBirthProps, DateOfBirthOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS - walletAdditionalDetails: { - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, - }, -})(DateOfBirthStep); +export default DateOfBirthStep; diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx index beccda648f9b..94ddb359e167 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -16,14 +15,6 @@ import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; -import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; - -type FullNameOnyxProps = { - /** Wallet Additional Details from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; -}; - -type FullNameProps = FullNameOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME]; @@ -40,10 +31,12 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL return errors; }; -function FullNameStep({walletAdditionalDetails, onNext, isEditing}: FullNameProps) { +function FullNameStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); + const defaultValues = { firstName: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.FIRST_NAME] ?? '', lastName: walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', @@ -96,9 +89,4 @@ function FullNameStep({walletAdditionalDetails, onNext, isEditing}: FullNameProp FullNameStep.displayName = 'FullNameStep'; -export default withOnyx<FullNameProps, FullNameOnyxProps>({ - // @ts-expect-error: ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS - walletAdditionalDetails: { - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, - }, -})(FullNameStep); +export default FullNameStep; diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx index 7767cea7a715..083f2d561570 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -16,14 +15,6 @@ import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; -import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; - -type PhoneNumberOnyxProps = { - /** Reimbursement account from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; -}; - -type PhoneNumberProps = PhoneNumberOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.PHONE_NUMBER]; @@ -36,10 +27,12 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL } return errors; }; -function PhoneNumberStep({walletAdditionalDetails, onNext, isEditing}: PhoneNumberProps) { +function PhoneNumberStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); + const defaultPhoneNumber = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.PHONE_NUMBER] ?? ''; const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ @@ -82,9 +75,4 @@ function PhoneNumberStep({walletAdditionalDetails, onNext, isEditing}: PhoneNumb PhoneNumberStep.displayName = 'PhoneNumberStep'; -export default withOnyx<PhoneNumberProps, PhoneNumberOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS - walletAdditionalDetails: { - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, - }, -})(PhoneNumberStep); +export default PhoneNumberStep; diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx index f0daa3d36110..c992a5edf533 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -16,14 +15,6 @@ import HelpLinks from '@pages/ReimbursementAccount/PersonalInfo/HelpLinks'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; -import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; - -type SocialSecurityNumberOnyxProps = { - /** Reimbursement account from ONYX */ - walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; -}; - -type SocialSecurityNumberProps = SocialSecurityNumberOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEY = INPUT_IDS.PERSONAL_INFO_STEP; const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.SSN_LAST_4]; @@ -37,10 +28,12 @@ const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.WALLET_ADDITIONAL return errors; }; -function SocialSecurityNumberStep({walletAdditionalDetails, onNext, isEditing}: SocialSecurityNumberProps) { +function SocialSecurityNumberStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); + const defaultSsnLast4 = walletAdditionalDetails?.[PERSONAL_INFO_STEP_KEY.SSN_LAST_4] ?? ''; const handleSubmit = useWalletAdditionalDetailsStepFormSubmit({ @@ -83,9 +76,4 @@ function SocialSecurityNumberStep({walletAdditionalDetails, onNext, isEditing}: SocialSecurityNumberStep.displayName = 'SocialSecurityNumberStep'; -export default withOnyx<SocialSecurityNumberProps, SocialSecurityNumberOnyxProps>({ - // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS - walletAdditionalDetails: { - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, - }, -})(SocialSecurityNumberStep); +export default SocialSecurityNumberStep; From c001154779c1dab2af01fd4f36513764b51e0201 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 18 Apr 2024 15:11:00 +0200 Subject: [PATCH 218/580] fix: minor fix --- src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 45e3561587b3..5de79bd80f46 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -1,7 +1,6 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import {useOnyx, withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -9,7 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; -// TODO: uncomment in the next PR +// TODO: uncomment after connecting steps https://github.com/Expensify/App/issues/36648 // import {parsePhoneNumber} from '@libs/PhoneNumber'; import Navigation from '@navigation/Navigation'; import getInitialSubstepForPersonalInfo from '@pages/EnablePayments/utils/getInitialSubstepForPersonalInfo'; @@ -18,9 +17,7 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {WalletAdditionalDetailsForm} from '@src/types/form'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; -import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; import Address from './substeps/AddressStep'; import Confirmation from './substeps/ConfirmationStep'; import DateOfBirth from './substeps/DateOfBirthStep'; @@ -40,7 +37,7 @@ function PersonalInfoPage() { const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, walletAdditionalDetailsDraft, walletAdditionalDetails), [walletAdditionalDetails, walletAdditionalDetailsDraft]); const submit = () => { - // TODO: uncomment in one of the next PR https://github.com/Expensify/App/issues/36648 + // TODO: uncomment after connecting steps https://github.com/Expensify/App/issues/36648 // const personalDetails = { // phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', // legalFirstName: values?.[PERSONAL_INFO_STEP_KEYS.FIRST_NAME] ?? '', From 386699844e8510e950d72e4324f3d809d146a188 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 18 Apr 2024 15:11:39 +0200 Subject: [PATCH 219/580] fix: minor fix --- src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 5de79bd80f46..0696d0591281 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -71,7 +71,7 @@ function PersonalInfoPage() { }); const handleBackButtonPress = () => { - // TODO: connect to the fist step of the wallet setup + // TODO: connect to the fist step of the wallet setup https://github.com/Expensify/App/issues/36648 if (screenIndex === 0) { Navigation.goBack(ROUTES.SETTINGS_WALLET); Wallet.resetWalletAdditionalDetailsDraft(); From 09a118c01e38e95cd001db9cbb328e69b87c1971 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 18 Apr 2024 15:18:07 +0200 Subject: [PATCH 220/580] Consierge read only --- src/libs/actions/Report.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1faf850b6d76..3f683bc2c55a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3142,6 +3142,7 @@ function completeOnboarding( managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, isOptimisticReport: true, + permissions: [CONST.REPORT.PERMISSIONS.READ], }, }, { @@ -3176,6 +3177,7 @@ function completeOnboarding( key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, value: { lastMentionedTime: DateUtils.getDBTime(), + permissions: [CONST.REPORT.PERMISSIONS.READ], }, }, { From 0c4209d0107944ac2299f04d3573e6678afbc7f3 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Thu, 18 Apr 2024 17:40:36 +0200 Subject: [PATCH 221/580] Fix to follow typing rules in ROUTES --- src/ROUTES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 48aa2f654728..a13ca074d144 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -329,7 +329,7 @@ const ROUTES = { MONEY_REQUEST_STEP_SEND_FROM: { route: 'create/:iouType/from/:transactionID/:reportID', getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/from/${transactionID}/${reportID}`, backTo), + getUrlWithBackToParam(`create/${iouType as string}/from/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_CONFIRMATION: { route: ':action/:iouType/confirmation/:transactionID/:reportID', From 94e18ada20c16a29e16a27d6990a12d99461e6d6 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 19 Apr 2024 00:36:32 +0700 Subject: [PATCH 222/580] resolve conflict --- src/pages/iou/request/step/IOURequestStepScan/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index d007881f9679..8bca59b11580 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -234,7 +234,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report); const participants = selectedParticipants.map((participant) => { const participantAccountID = participant?.accountID ?? 0; return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); From 517ad3afea3e804a328c97b33fb5862d4fcdc618 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 19 Apr 2024 01:00:46 +0700 Subject: [PATCH 223/580] Add backTo param and disable edit payer for policy expense chat --- src/ROUTES.ts | 4 ++-- ...neyTemporaryForRefactorRequestConfirmationList.tsx | 8 ++++---- src/libs/Navigation/types.ts | 1 + .../iou/request/step/IOURequestStepSplitPayer.tsx | 11 ++++++++--- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 55b8729bf249..1dd6dc1cfe29 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -388,8 +388,8 @@ const ROUTES = { }, MONEY_REQUEST_STEP_SPLIT_PAYER: { route: ':action/:iouType/confirmation/:transactionID/:reportID/payer', - getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, action: ValueOf<typeof CONST.IOU.ACTION> = 'create') => - `${action}/${iouType}/confirmation/${transactionID}/${reportID}/payer` as const, + getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '', action: ValueOf<typeof CONST.IOU.ACTION> = 'create') => + getUrlWithBackToParam(`${action}/${iouType}/confirmation/${transactionID}/${reportID}/payer`, backTo), }, MONEY_REQUEST_STEP_SCAN: { route: ':action/:iouType/scan/:transactionID/:reportID', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx index 97c34c58fa6d..1de4e3e879d9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.tsx @@ -643,18 +643,18 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( <MenuItem label={translate('moneyRequestConfirmationList.paidBy')} - interactive={!transaction?.isFromGlobalCreate} + interactive={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate} description={personalDetailsOfPayee.login} title={personalDetailsOfPayee.displayName} icon={payeeIcons} onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(iouType, transaction?.transactionID ?? '', reportID)); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); }} - shouldShowRightIcon + shouldShowRightIcon={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate} titleWithTooltips={payeeTooltipDetails} /> ), - shouldShow: isTypeSplit, + shouldShow: isTypeSplit && action === CONST.IOU.ACTION.CREATE, isSupplementary: false, }, { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 7e25eda12bb8..7834fdf40b7b 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -440,6 +440,7 @@ type MoneyRequestNavigatorParamList = { iouType: ValueOf<typeof CONST.IOU.TYPE>; transactionID: string; reportID: string; + backTo: Routes; }; [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: undefined; [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: undefined; diff --git a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx index 8327e37579a9..dc1a958ed538 100644 --- a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx +++ b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx @@ -9,7 +9,9 @@ import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import type {OptionData} from '@libs/ReportUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; @@ -25,9 +27,10 @@ type IOURequestStepSplitPayerProps = WithWritableReportOrNotFoundProps<typeof SC function IOURequestStepSplitPayer({ route: { - params: {iouType, transactionID}, + params: {iouType, transactionID, action, backTo}, }, transaction, + report, }: IOURequestStepSplitPayerProps) { const {translate} = useLocalize(); const personalDetails = usePersonalDetails(); @@ -52,7 +55,7 @@ function IOURequestStepSplitPayer({ }, [transaction?.participants, personalDetails, transaction?.splitPayerAccountIDs]); const navigateBack = () => { - Navigation.goBack(); + Navigation.goBack(backTo); }; const setSplitPayer = (item: Participant | OptionData) => { @@ -64,7 +67,9 @@ function IOURequestStepSplitPayer({ <StepScreenWrapper headerTitle={translate('moneyRequestConfirmationList.paidBy')} onBackButtonPress={navigateBack} - shouldShowNotFoundPage={!IOUUtils.isValidMoneyRequestType(iouType)} + shouldShowNotFoundPage={ + !IOUUtils.isValidMoneyRequestType(iouType) || ReportUtils.isPolicyExpenseChat(report) || action !== CONST.IOU.ACTION.CREATE || iouType !== CONST.IOU.TYPE.SPLIT + } shouldShowWrapper testID={IOURequestStepSplitPayer.displayName} > From cd917738465482115bf7eb2e738f3bd0c0dada5a Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Fri, 19 Apr 2024 01:14:21 +0530 Subject: [PATCH 224/580] show empty receipt for admin & approver if not present. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptEmptyState.tsx | 5 ++++- src/components/ReportActionItem/MoneyRequestView.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/ReceiptEmptyState.tsx b/src/components/ReceiptEmptyState.tsx index 9884e97a3fa0..abc025814096 100644 --- a/src/components/ReceiptEmptyState.tsx +++ b/src/components/ReceiptEmptyState.tsx @@ -12,10 +12,12 @@ type ReceiptEmptyStateProps = { /** Callback to be called on onPress */ onPress?: () => void; + + disabled?: boolean; }; // Returns an SVG icon indicating that the user should attach a receipt -function ReceiptEmptyState({hasError = false, onPress = () => {}}: ReceiptEmptyStateProps) { +function ReceiptEmptyState({hasError = false, onPress = () => {}, disabled = false}: ReceiptEmptyStateProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -24,6 +26,7 @@ function ReceiptEmptyState({hasError = false, onPress = () => {}}: ReceiptEmptyS accessibilityRole="imagebutton" accessibilityLabel={translate('receipt.upload')} onPress={onPress} + disabled={disabled} style={[styles.alignItemsCenter, styles.justifyContentCenter, styles.moneyRequestViewImage, styles.moneyRequestAttachReceipt, hasError && styles.borderColorDanger]} > <Icon diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 53c89b52eb5f..2849deb9b798 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -6,6 +6,7 @@ import ConfirmedRoute from '@components/ConfirmedRoute'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {useSession} from '@components/OnyxProvider'; import {ReceiptAuditHeader, ReceiptAuditMessages} from '@components/ReceiptAudit'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; import SpacerView from '@components/SpacerView'; @@ -93,6 +94,7 @@ function MoneyRequestView({ }: MoneyRequestViewProps) { const theme = useTheme(); const styles = useThemeStyles(); + const session = useSession(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); @@ -147,6 +149,8 @@ function MoneyRequestView({ const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); + const isAdmin = policy?.role === 'admin'; + const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); @@ -298,9 +302,10 @@ function MoneyRequestView({ </View> </OfflineWithFeedback> )} - {!hasReceipt && canEditReceipt && ( + {!hasReceipt && (canEditReceipt || isAdmin || isApprover) && ( <ReceiptEmptyState hasError={hasErrors} + disabled={!canEditReceipt} onPress={() => Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( From 34149a4d9d903b89d6dab5b0407d6dd1c17acca1 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 19 Apr 2024 14:34:54 +0700 Subject: [PATCH 225/580] move logic to new page and update translation --- .../MoneyRequestConfirmationList.tsx | 90 ++++++++++--------- src/languages/es.ts | 2 +- .../step/IOURequestStepConfirmation.tsx | 3 +- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e90cb7584c43..62544689796c 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -27,6 +27,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; +import * as UserUtils from '@libs/UserUtils'; import * as IOU from '@userActions/IOU'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; @@ -41,7 +42,9 @@ import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; +import MenuItem from './MenuItem'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import {usePersonalDetails} from './OnyxProvider'; import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; @@ -214,6 +217,7 @@ function MoneyRequestConfirmationList({ const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const personalDetails = usePersonalDetails(); const {canUseViolations} = usePermissions(); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; @@ -350,8 +354,13 @@ function MoneyRequestConfirmationList({ */ const getParticipantsWithAmount = useCallback( (participantsList: Participant[]) => { - const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); + const amount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? ''); + const myAmount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? '', true); + return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( + participantsList, + amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '', + myAmount > 0 ? CurrencyUtils.convertToDisplayString(myAmount, iouCurrencyCode) : '', + ); }, [iouAmount, iouCurrencyCode], ); @@ -386,8 +395,16 @@ function MoneyRequestConfirmationList({ const selectedParticipants = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); + const payeeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs([payeePersonalDetails.accountID], personalDetails), false); + const payeeIcons = [ + { + source: UserUtils.getAvatar(payeePersonalDetails.avatar, payeePersonalDetails.accountID), + name: payeePersonalDetails.login ?? '', + type: CONST.ICON_TYPE_AVATAR, + id: payeePersonalDetails.accountID, + }, + ]; const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; - const shouldDisablePaidBySection = canModifyParticipants; const optionSelectorSections = useMemo(() => { const sections = []; const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); @@ -402,25 +419,11 @@ function MoneyRequestConfirmationList({ })); } - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); - const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( - payeePersonalDetails, - iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', - ); - - sections.push( - { - title: translate('moneyRequestConfirmationList.paidBy'), - data: [formattedPayeeOption], - shouldShow: true, - isDisabled: shouldDisablePaidBySection, - }, - { - title: translate('moneyRequestConfirmationList.splitWith'), - data: formattedParticipantsList, - shouldShow: true, - }, - ); + sections.push({ + title: translate('moneyRequestConfirmationList.splitAmounts'), + data: formattedParticipantsList, + shouldShow: true, + }); } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, @@ -433,25 +436,14 @@ function MoneyRequestConfirmationList({ }); } return sections; - }, [ - selectedParticipants, - selectedParticipantsProp, - hasMultipleParticipants, - iouAmount, - iouCurrencyCode, - getParticipantsWithAmount, - payeePersonalDetails, - translate, - shouldDisablePaidBySection, - canModifyParticipants, - ]); + }, [selectedParticipants, selectedParticipantsProp, hasMultipleParticipants, getParticipantsWithAmount, translate, canModifyParticipants]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { return []; } - return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; - }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]); + return [...selectedParticipants]; + }, [selectedParticipants, hasMultipleParticipants]); useEffect(() => { if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { @@ -514,12 +506,12 @@ function MoneyRequestConfirmationList({ const selectParticipant = useCallback( (option: Participant) => { // Return early if selected option is currently logged in user. - if (option.accountID === session?.accountID) { + if (option.accountID === session?.accountID || option.accountID === payeePersonalDetails.accountID) { return; } onSelectParticipant?.(option); }, - [session?.accountID, onSelectParticipant], + [session?.accountID, onSelectParticipant, payeePersonalDetails.accountID], ); /** @@ -609,7 +601,7 @@ function MoneyRequestConfirmationList({ } const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0; + const shouldDisableButton = isTypeSplit ? selectedParticipants.length === 1 : selectedParticipants.length === 0; const button = shouldShowSettlementButton ? ( <SettlementButton @@ -657,11 +649,29 @@ function MoneyRequestConfirmationList({ {button} </> ); - }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2, isTypeSplit]); // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ + { + item: ( + <MenuItem + label={translate('moneyRequestConfirmationList.paidBy')} + interactive={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate} + description={payeePersonalDetails.login} + title={payeePersonalDetails.displayName} + icon={payeeIcons} + onPress={() => { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); + }} + shouldShowRightIcon={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate} + titleWithTooltips={payeeTooltipDetails} + /> + ), + shouldShow: isTypeSplit && action === CONST.IOU.ACTION.CREATE, + isSupplementary: false, + }, { item: ( <MenuItemWithTopDescription diff --git a/src/languages/es.ts b/src/languages/es.ts index 7f8e40bbf26b..6bac6abba903 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -384,7 +384,7 @@ export default { moneyRequestConfirmationList: { paidBy: 'Pagado por', splitWith: 'Dividir con', - splitAmounts: 'Dividir importes', + splitAmounts: 'Importes a dividir', whatsItFor: '¿Para qué es?', }, optionsSelector: { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 46e7fecf3fa0..a1da46605580 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -77,7 +77,6 @@ function IOURequestStepConfirmation({ const transactionTaxAmount = transaction?.taxAmount; const isSharingTrackExpense = action === CONST.IOU.ACTION.SHARE; const isCategorizingTrackExpense = action === CONST.IOU.ACTION.CATEGORIZE; - const isRequestingFromTrackExpense = action === CONST.IOU.ACTION.MOVE; const payeePersonalDetails = personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1]; const isRequestingFromTrackExpense = action === CONST.IOU.ACTION.REQUEST; @@ -122,7 +121,7 @@ function IOURequestStepConfirmation({ } const payeeParticipant = OptionsListUtils.getParticipantsOption({accountID: payeePersonalDetails?.accountID, selected: true}, personalDetails); - IOU.setMoneyRequestParticipants_temporaryForRefactor(transaction.transactionID, [...(transaction?.participants ?? []), payeeParticipant]); + IOU.setMoneyRequestParticipants_temporaryForRefactor(transaction.transactionID, [payeeParticipant, ...(transaction?.participants ?? [])]); // We only want to run it when the component is mounted // eslint-disable-next-line react-hooks/exhaustive-deps From 0738a3e106ce8f374dde6a02229ea808d4e00745 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 19 Apr 2024 10:24:30 +0200 Subject: [PATCH 226/580] Update param invoiceRoomID -> invoiceRoomReportID --- src/libs/API/parameters/SendInvoiceParams.ts | 2 +- src/libs/actions/IOU.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/API/parameters/SendInvoiceParams.ts b/src/libs/API/parameters/SendInvoiceParams.ts index 5e65968654e6..5ede0dcaa920 100644 --- a/src/libs/API/parameters/SendInvoiceParams.ts +++ b/src/libs/API/parameters/SendInvoiceParams.ts @@ -8,7 +8,7 @@ type SendInvoiceParams = { merchant: string; date: string; category?: string; - invoiceRoomID?: string; + invoiceRoomReportID?: string; createdChatReportActionID: string; invoiceReportID: string; reportPreviewReportActionID: string; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 097ee96b8ded..8a3c543813ec 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -99,7 +99,7 @@ type TrackExpenseInformation = { type SendInvoiceInformation = { senderWorkspaceID: string; receiver: Partial<OnyxTypes.PersonalDetails>; - invoiceRoomID: string; + invoiceRoomReportID: string; createdChatReportActionID: string; invoiceReportID: string; reportPreviewReportActionID: string; @@ -1756,7 +1756,7 @@ function getSendInvoiceInformation( return { senderWorkspaceID, receiver, - invoiceRoomID: chatReport.reportID, + invoiceRoomReportID: chatReport.reportID, createdChatReportActionID: optimisticCreatedActionForChat.reportActionID, invoiceReportID: optimisticInvoiceReport.reportID, reportPreviewReportActionID: reportPreviewAction.reportActionID, @@ -3366,7 +3366,7 @@ function sendInvoice( policyTagList?: OnyxEntry<OnyxTypes.PolicyTagList>, policyCategories?: OnyxEntry<OnyxTypes.PolicyCategories>, ) { - const {senderWorkspaceID, receiver, invoiceRoomID, createdChatReportActionID, invoiceReportID, reportPreviewReportActionID, transactionID, transactionThreadReportID, onyxData} = + const {senderWorkspaceID, receiver, invoiceRoomReportID, createdChatReportActionID, invoiceReportID, reportPreviewReportActionID, transactionID, transactionThreadReportID, onyxData} = getSendInvoiceInformation(transaction, currentUserAccountID, invoiceChatReport, receiptFile, policy, policyTagList, policyCategories); let parameters: SendInvoiceParams = { @@ -3377,7 +3377,7 @@ function sendInvoice( merchant: transaction?.merchant ?? '', category: transaction?.category, date: transaction?.created ?? '', - invoiceRoomID, + invoiceRoomReportID, createdChatReportActionID, invoiceReportID, reportPreviewReportActionID, @@ -3400,8 +3400,8 @@ function sendInvoice( API.write(WRITE_COMMANDS.SEND_INVOICE, parameters, onyxData); resetMoneyRequestInfo(); - Navigation.dismissModal(invoiceRoomID); - Report.notifyNewAction(invoiceRoomID, receiver.accountID); + Navigation.dismissModal(invoiceRoomReportID); + Report.notifyNewAction(invoiceRoomReportID, receiver.accountID); } /** From 272b35d134e52133f2c129874f370984e3fe83d0 Mon Sep 17 00:00:00 2001 From: GandalfGwaihir <whogandalf@gmail.com> Date: Fri, 19 Apr 2024 14:35:31 +0530 Subject: [PATCH 227/580] Update comments based on suggestions --- src/pages/iou/HoldReasonPage.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 6e31f6202383..ad488f6cfad2 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -47,8 +47,8 @@ function HoldReasonPage({route}: HoldReasonPageProps) { const report = ReportUtils.getReport(reportID); - // We check if the report is part of a policy, if not then it's a personal request (1:1 request) - // We need to allow both users in the 1:1 request to put the request on hold + // We first check if the report is part of a policy - if not, then it's a personal request (1:1 request) + // For personal requests, we need to allow both users to put the request on hold const isWorkspaceRequest = ReportUtils.isGroupPolicy(report); const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); @@ -57,9 +57,9 @@ function HoldReasonPage({route}: HoldReasonPageProps) { }; const onSubmit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.MONEY_REQUEST_HOLD_FORM>) => { - // We have extra isWorkspaceRequest condition as in case of 1:1 request, canEditMoneyRequest will rightly return false - // as we do not allow requestee to edit fields like description and amount, - // but we still want the requestee to be able to put the request on hold + // We have extra isWorkspaceRequest condition since, for 1:1 requests, canEditMoneyRequest will rightly return false + // as we do not allow requestee to edit fields like description and amount. + // But, we still want the requestee to be able to put the request on hold if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { return; } @@ -75,9 +75,9 @@ function HoldReasonPage({route}: HoldReasonPageProps) { if (!values.comment) { errors.comment = 'common.error.fieldRequired'; } - // We have extra isWorkspaceRequest condition as in case of 1:1 request, canEditMoneyRequest will rightly return false - // as we do not allow requestee to edit fields like description and amount, - // but we still want the requestee to be able to put the request on hold + // We have extra isWorkspaceRequest condition since, for 1:1 requests, canEditMoneyRequest will rightly return false + // as we do not allow requestee to edit fields like description and amount. + // But, we still want the requestee to be able to put the request on hold if (!ReportUtils.canEditMoneyRequest(parentReportAction) && isWorkspaceRequest) { const formErrors = {}; ErrorUtils.addErrorMessage(formErrors, 'reportModified', 'common.error.requestModified'); From a3c0e383757b8ec618b66a86c192dd38dd467a20 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 19 Apr 2024 11:18:05 +0200 Subject: [PATCH 228/580] Updates to follow main branch changes --- .../MoneyRequestConfirmationList.tsx | 45 +++++++++++++++++-- src/libs/ReportUtils.ts | 2 +- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index e90cb7584c43..260b426ab61b 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -5,7 +5,8 @@ import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'reac import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import MenuItem from '@components/MenuItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -24,6 +25,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; @@ -66,6 +68,9 @@ type MoneyRequestConfirmationListOnyxProps = { /** Unit and rate used for if the expense is a distance expense */ mileageRate: OnyxEntry<DefaultMileageRate>; + + /** The list of all policies */ + allPolicies: OnyxCollection<OnyxTypes.Policy>; }; type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps & { @@ -208,6 +213,7 @@ function MoneyRequestConfirmationList({ onToggleBillable, hasSmartScanFailed, reportActionID, + allPolicies, action = CONST.IOU.ACTION.CREATE, }: MoneyRequestConfirmationListProps) { const theme = useTheme(); @@ -220,6 +226,7 @@ function MoneyRequestConfirmationList({ const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; + const isTypeInvoice = iouType === CONST.IOU.TYPE.INVOICE; const {unit, rate, currency} = mileageRate ?? { unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, @@ -246,6 +253,13 @@ function MoneyRequestConfirmationList({ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + const senderWorkspace = useMemo(() => { + const senderWorkspaceParticipant = selectedParticipantsProp.find((participant) => participant.policyID); + return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant?.policyID}`]; + }, [allPolicies, selectedParticipantsProp]); + + const canUpdateSenderWorkspace = useMemo(() => PolicyUtils.canSendInvoice(allPolicies) && !!transaction?.isFromGlobalCreate, [allPolicies, transaction?.isFromGlobalCreate]); + // A flag for showing the tags field const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]); @@ -363,7 +377,9 @@ function MoneyRequestConfirmationList({ const splitOrRequestOptions: Array<DropdownOption<string>> = useMemo(() => { let text; - if (isTypeTrackExpense) { + if (isTypeInvoice) { + text = translate('iou.sendInvoice', {amount: formattedAmount}); + } else if (isTypeTrackExpense) { text = translate('iou.trackExpense'); } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.splitExpense'); @@ -382,7 +398,7 @@ function MoneyRequestConfirmationList({ value: iouType, }, ]; - }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); + }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount, isTypeInvoice]); const selectedParticipants = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); @@ -937,6 +953,26 @@ function MoneyRequestConfirmationList({ <ConfirmedRoute transaction={transaction ?? ({} as OnyxTypes.Transaction)} /> </View> )} + {isTypeInvoice && ( + <MenuItem + key={translate('workspace.invoices.sendFrom')} + shouldShowRightIcon={!isReadOnly && canUpdateSenderWorkspace} + title={senderWorkspace?.name} + icon={senderWorkspace?.avatar ? senderWorkspace?.avatar : getDefaultWorkspaceAvatar(senderWorkspace?.name)} + iconType={CONST.ICON_TYPE_WORKSPACE} + description={translate('workspace.common.workspace')} + label={translate('workspace.invoices.sendFrom')} + isLabelHoverable={false} + interactive={!isReadOnly && canUpdateSenderWorkspace} + onPress={() => { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SEND_FROM.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); + }} + style={styles.moneyRequestMenuItem} + labelStyle={styles.mt2} + titleStyle={styles.flex1} + disabled={didConfirm || !canUpdateSenderWorkspace} + /> + )} {(!isMovingTransactionFromTrackExpense || !hasRoute) && // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing (receiptImage || receiptThumbnail @@ -993,4 +1029,7 @@ export default withOnyx<MoneyRequestConfirmationListProps, MoneyRequestConfirmat policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, })(MoneyRequestConfirmationList); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 024593837fae..e898439dc844 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3215,7 +3215,7 @@ function buildOptimisticInviteReportAction(invitedUserDisplayName: string, invit return { reportActionID: NumberUtils.rand64(), - actionName: CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM, + actionName: CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.INVITE_TO_ROOM, actorAccountID: currentUserAccountID, person: [ { From 4dd76993d43697b5060d26a029a2f3c05967cc9b Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 19 Apr 2024 11:38:30 +0200 Subject: [PATCH 229/580] Lint fix --- src/components/MoneyRequestConfirmationList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 260b426ab61b..ff5726a33966 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -6,7 +6,6 @@ import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import MenuItem from '@components/MenuItem'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -43,6 +42,7 @@ import type {DropdownOption} from './ButtonWithDropdownMenu/types'; import ConfirmedRoute from './ConfirmedRoute'; import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; +import MenuItem from './MenuItem'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import OptionsSelector from './OptionsSelector'; import PDFThumbnail from './PDFThumbnail'; From 4d1523906c0ac3f21d1697d245530a919d3dceed Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Fri, 19 Apr 2024 12:05:39 +0200 Subject: [PATCH 230/580] merge admin and paid policy wrappers --- .../workspace/AccessOrNotFoundWrapper.tsx | 80 +++++++++ .../workspace/WorkspaceMoreFeaturesPage.tsx | 37 ++-- .../accounting/PolicyAccountingPage.tsx | 105 ++++++------ .../qbo/QuickbooksChartOfAccountsPage.tsx | 9 +- .../accounting/qbo/QuickbooksClassesPage.tsx | 9 +- .../qbo/QuickbooksCustomersPage.tsx | 9 +- .../accounting/qbo/QuickbooksImportPage.tsx | 9 +- .../qbo/QuickbooksLocationsPage.tsx | 9 +- .../accounting/qbo/QuickbooksTaxesPage.tsx | 9 +- .../categories/CategorySettingsPage.tsx | 113 ++++++------- .../categories/CreateCategoryPage.tsx | 47 +++--- .../workspace/categories/EditCategoryPage.tsx | 51 +++--- .../categories/WorkspaceCategoriesPage.tsx | 129 +++++++------- .../WorkspaceCategoriesSettingsPage.tsx | 73 ++++---- .../distanceRates/CreateDistanceRatePage.tsx | 73 ++++---- .../PolicyDistanceRateDetailsPage.tsx | 137 ++++++++------- .../PolicyDistanceRateEditPage.tsx | 81 +++++---- .../distanceRates/PolicyDistanceRatesPage.tsx | 127 +++++++------- .../PolicyDistanceRatesSettingsPage.tsx | 81 +++++---- .../members/WorkspaceMemberDetailsPage.tsx | 159 +++++++++--------- .../members/WorkspaceOwnerChangeErrorPage.tsx | 69 ++++---- .../WorkspaceOwnerChangeSuccessPage.tsx | 39 ++--- .../WorkspaceOwnerChangeWrapperPage.tsx | 55 +++--- src/pages/workspace/tags/EditTagPage.tsx | 73 ++++---- src/pages/workspace/tags/TagSettingsPage.tsx | 129 +++++++------- .../workspace/tags/WorkspaceCreateTagPage.tsx | 71 ++++---- .../workspace/tags/WorkspaceEditTagsPage.tsx | 69 ++++---- .../workspace/tags/WorkspaceTagsPage.tsx | 129 +++++++------- .../tags/WorkspaceTagsSettingsPage.tsx | 95 +++++------ src/pages/workspace/taxes/NamePage.tsx | 81 +++++---- src/pages/workspace/taxes/ValuePage.tsx | 87 +++++----- .../taxes/WorkspaceCreateTaxPage.tsx | 111 ++++++------ .../workspace/taxes/WorkspaceEditTaxPage.tsx | 155 +++++++++-------- .../workspace/taxes/WorkspaceTaxesPage.tsx | 117 +++++++------ .../WorkspaceTaxesSettingsCustomTaxName.tsx | 79 +++++---- .../WorkspaceTaxesSettingsForeignCurrency.tsx | 57 +++---- .../taxes/WorkspaceTaxesSettingsPage.tsx | 67 ++++---- ...orkspaceTaxesSettingsWorkspaceCurrency.tsx | 57 +++---- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 61 ++++--- 39 files changed, 1475 insertions(+), 1473 deletions(-) create mode 100644 src/pages/workspace/AccessOrNotFoundWrapper.tsx diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx new file mode 100644 index 000000000000..333aba514168 --- /dev/null +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -0,0 +1,80 @@ +/* eslint-disable rulesdir/no-negated-variables */ +import React, {useEffect} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import * as Policy from '@userActions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +const POLICY_ACCESS_VARIANTS = { + PAID: (policy: OnyxEntry<OnyxTypes.Policy>) => !PolicyUtils.isPaidGroupPolicy(policy) || !policy?.isPolicyExpenseChatEnabled, + ADMIN: (policy: OnyxEntry<OnyxTypes.Policy>) => !PolicyUtils.isPolicyAdmin(policy), +} as const satisfies Record<string, (policy: OnyxTypes.Policy) => boolean>; + +type PolicyAccessVariant = keyof typeof POLICY_ACCESS_VARIANTS; + +type AccessOrNotFoundWrapperOnyxProps = { + /** The report currently being looked at */ + policy: OnyxEntry<OnyxTypes.Policy>; + + /** Indicated whether the report data is loading */ + isLoadingReportData: OnyxEntry<boolean>; +}; + +type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { + /** The children to render */ + children: ((props: AccessOrNotFoundWrapperOnyxProps) => React.ReactNode) | React.ReactNode; + + /** The report currently being looked at */ + policyID: string; + + /** Defines which types of access should be verified */ + accessVariants?: PolicyAccessVariant[]; +}; + +function AccessOrNotFoundWrapper({accessVariants = ['ADMIN', 'PAID'], ...props}: AccessOrNotFoundWrapperProps) { + const isPolicyIDInRoute = !!props.policyID?.length; + + useEffect(() => { + if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { + // If the workspace is not required or is already loaded, we don't need to call the API + return; + } + + Policy.openWorkspace(props.policyID, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPolicyIDInRoute, props.policyID]); + + const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + + const pageUnaccessible = accessVariants.reduce((acc, variant) => { + const accessFunction = POLICY_ACCESS_VARIANTS[variant]; + return acc || accessFunction(props.policy); + }, false); + const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || pageUnaccessible; + + if (shouldShowFullScreenLoadingIndicator) { + return <FullscreenLoadingIndicator />; + } + + if (shouldShowNotFoundPage) { + return <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; + } + + return typeof props.children === 'function' ? props.children(props) : props.children; +} + +export default withOnyx<AccessOrNotFoundWrapperProps, AccessOrNotFoundWrapperOnyxProps>({ + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, + }, + isLoadingReportData: { + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + }, +})(AccessOrNotFoundWrapper); diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 66e6b87d79d3..46603c0712de 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -18,8 +18,7 @@ import type {TranslationPaths} from '@src/languages/types'; import type SCREENS from '@src/SCREENS'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type IconAsset from '@src/types/utils/IconAsset'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from './PaidPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import ToggleSettingOptionRow from './workflows/ToggleSettingsOptionRow'; @@ -196,24 +195,22 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceMoreFeaturesPage.displayName} - shouldShowOfflineIndicatorInWideScreen - > - <HeaderWithBackButton - icon={Illustrations.Gears} - title={translate('workspace.common.moreFeatures')} - shouldShowBackButton={isSmallScreenWidth} - /> - - <ScrollView contentContainerStyle={styles.pb2}>{sections.map(renderSection)}</ScrollView> - </ScreenWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceMoreFeaturesPage.displayName} + shouldShowOfflineIndicatorInWideScreen + > + <HeaderWithBackButton + icon={Illustrations.Gears} + title={translate('workspace.common.moreFeatures')} + shouldShowBackButton={isSmallScreenWidth} + /> + + <ScrollView contentContainerStyle={styles.pb2}>{sections.map(renderSection)}</ScrollView> + </ScreenWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index d7907b7a91b6..c6b91ec37e97 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -20,9 +20,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {removePolicyConnection} from '@libs/actions/connections'; import Navigation from '@navigation/Navigation'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {AnchorPosition} from '@styles/index'; @@ -175,59 +174,57 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting ]; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + > + <ScreenWrapper + testID={PolicyAccountingPage.displayName} + includeSafeAreaPaddingBottom={false} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - testID={PolicyAccountingPage.displayName} - includeSafeAreaPaddingBottom={false} - shouldShowOfflineIndicatorInWideScreen - > - <HeaderWithBackButton - title={translate('workspace.common.accounting')} - shouldShowBackButton={isSmallScreenWidth} - icon={Illustrations.Accounting} - shouldShowThreeDotsButton - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} - threeDotsMenuItems={headerThreeDotsMenuItems} - /> - <ScrollView contentContainerStyle={styles.pt3}> - <View style={[styles.flex1, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}> - <Section - title={translate('workspace.accounting.title')} - subtitle={translate('workspace.accounting.subtitle')} - isCentralPane - subtitleMuted - titleStyles={styles.accountSettingsSectionTitle} - childrenStyles={styles.pt5} - > - <MenuItemList - menuItems={menuItems} - shouldUseSingleExecution - /> - </Section> - </View> - </ScrollView> - <ConfirmModal - title={translate('workspace.accounting.disconnectTitle')} - isVisible={isDisconnectModalOpen} - onConfirm={() => { - removePolicyConnection(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO); - setIsDisconnectModalOpen(false); - }} - onCancel={() => setIsDisconnectModalOpen(false)} - prompt={translate('workspace.accounting.disconnectPrompt')} - confirmText={translate('workspace.accounting.disconnect')} - cancelText={translate('common.cancel')} - danger - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <HeaderWithBackButton + title={translate('workspace.common.accounting')} + shouldShowBackButton={isSmallScreenWidth} + icon={Illustrations.Accounting} + shouldShowThreeDotsButton + threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} + threeDotsMenuItems={headerThreeDotsMenuItems} + /> + <ScrollView contentContainerStyle={styles.pt3}> + <View style={[styles.flex1, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}> + <Section + title={translate('workspace.accounting.title')} + subtitle={translate('workspace.accounting.subtitle')} + isCentralPane + subtitleMuted + titleStyles={styles.accountSettingsSectionTitle} + childrenStyles={styles.pt5} + > + <MenuItemList + menuItems={menuItems} + shouldUseSingleExecution + /> + </Section> + </View> + </ScrollView> + <ConfirmModal + title={translate('workspace.accounting.disconnectTitle')} + isVisible={isDisconnectModalOpen} + onConfirm={() => { + removePolicyConnection(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO); + setIsDisconnectModalOpen(false); + }} + onCancel={() => setIsDisconnectModalOpen(false)} + prompt={translate('workspace.accounting.disconnectPrompt')} + confirmText={translate('workspace.accounting.disconnect')} + cancelText={translate('common.cancel')} + danger + /> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage.tsx index 10f7500b3ca1..8401d0deb9ab 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage.tsx @@ -9,7 +9,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -24,7 +24,10 @@ function QuickbooksChartOfAccountsPage({policy}: WithPolicyProps) { const isSwitchOn = Boolean(enableNewCategories && enableNewCategories !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN']} + policyID={policyID} + > <FeatureEnabledAccessOrNotFoundWrapper policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} @@ -64,7 +67,7 @@ function QuickbooksChartOfAccountsPage({policy}: WithPolicyProps) { </ScrollView> </ScreenWrapper> </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksClassesPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksClassesPage.tsx index 62a0a65ac91c..c16ba8b687dc 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksClassesPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksClassesPage.tsx @@ -10,7 +10,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -26,7 +26,10 @@ function QuickbooksClassesPage({policy}: WithPolicyProps) { const isReportFieldsSelected = syncClasses === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN']} + policyID={policyID} + > <FeatureEnabledAccessOrNotFoundWrapper policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} @@ -73,7 +76,7 @@ function QuickbooksClassesPage({policy}: WithPolicyProps) { </ScrollView> </ScreenWrapper> </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksCustomersPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksCustomersPage.tsx index 3192ff4d83a9..05c4a5608e75 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksCustomersPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksCustomersPage.tsx @@ -10,7 +10,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -25,7 +25,10 @@ function QuickbooksCustomersPage({policy}: WithPolicyProps) { const isSwitchOn = Boolean(syncCustomers && syncCustomers !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const isReportFieldsSelected = syncCustomers === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN']} + policyID={policyID} + > <FeatureEnabledAccessOrNotFoundWrapper policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} @@ -72,7 +75,7 @@ function QuickbooksCustomersPage({policy}: WithPolicyProps) { </ScrollView> </ScreenWrapper> </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksImportPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksImportPage.tsx index c5003dcdd246..04bc26e9ff8c 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksImportPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksImportPage.tsx @@ -8,7 +8,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@navigation/Navigation'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -68,7 +68,10 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { ]; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN']} + policyID={policyID} + > <FeatureEnabledAccessOrNotFoundWrapper policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} @@ -98,7 +101,7 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { </ScrollView> </ScreenWrapper> </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksLocationsPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksLocationsPage.tsx index fa573b813585..22d689063b0a 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksLocationsPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksLocationsPage.tsx @@ -10,7 +10,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -26,7 +26,10 @@ function QuickbooksLocationsPage({policy}: WithPolicyProps) { const isReportFieldsSelected = syncLocations === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN']} + policyID={policyID} + > <FeatureEnabledAccessOrNotFoundWrapper policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} @@ -76,7 +79,7 @@ function QuickbooksLocationsPage({policy}: WithPolicyProps) { </ScrollView> </ScreenWrapper> </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksTaxesPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksTaxesPage.tsx index 215d8397b7ec..8745876f33ab 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksTaxesPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksTaxesPage.tsx @@ -9,7 +9,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -23,7 +23,10 @@ function QuickbooksTaxesPage({policy}: WithPolicyProps) { const {syncTaxes, pendingFields} = policy?.connections?.quickbooksOnline?.config ?? {}; const isSwitchOn = Boolean(syncTaxes && syncTaxes !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN']} + policyID={policyID} + > <FeatureEnabledAccessOrNotFoundWrapper policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} @@ -60,7 +63,7 @@ function QuickbooksTaxesPage({policy}: WithPolicyProps) { </ScrollView> </ScreenWrapper> </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 6e35c4e2d040..00ce700ecd36 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -19,9 +19,8 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -71,64 +70,62 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro ]; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={CategorySettingsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={CategorySettingsPage.displayName} - > - <HeaderWithBackButton - shouldShowThreeDotsButton - title={route.params.categoryName} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} - threeDotsMenuItems={threeDotsMenuItems} - /> - <ConfirmModal - isVisible={deleteCategoryConfirmModalVisible} - onConfirm={deleteCategory} - onCancel={() => setDeleteCategoryConfirmModalVisible(false)} - title={translate('workspace.categories.deleteCategory')} - prompt={translate('workspace.categories.deleteCategoryPrompt')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorMessageField(policyCategory)} - pendingAction={policyCategory?.pendingFields?.enabled} - errorRowStyles={styles.mh5} - onClose={() => Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)} - > - <View style={[styles.mt2, styles.mh5]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text style={[styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.enableCategory')}</Text> - <Switch - isOn={policyCategory.enabled} - accessibilityLabel={translate('workspace.categories.enableCategory')} - onToggle={updateWorkspaceRequiresCategory} - /> - </View> + <HeaderWithBackButton + shouldShowThreeDotsButton + title={route.params.categoryName} + threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} + threeDotsMenuItems={threeDotsMenuItems} + /> + <ConfirmModal + isVisible={deleteCategoryConfirmModalVisible} + onConfirm={deleteCategory} + onCancel={() => setDeleteCategoryConfirmModalVisible(false)} + title={translate('workspace.categories.deleteCategory')} + prompt={translate('workspace.categories.deleteCategoryPrompt')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorMessageField(policyCategory)} + pendingAction={policyCategory?.pendingFields?.enabled} + errorRowStyles={styles.mh5} + onClose={() => Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)} + > + <View style={[styles.mt2, styles.mh5]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text style={[styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.enableCategory')}</Text> + <Switch + isOn={policyCategory.enabled} + accessibilityLabel={translate('workspace.categories.enableCategory')} + onToggle={updateWorkspaceRequiresCategory} + /> </View> - </OfflineWithFeedback> - <OfflineWithFeedback pendingAction={policyCategory.pendingFields?.name}> - <MenuItemWithTopDescription - title={policyCategory.name} - description={translate(`workspace.categories.categoryName`)} - onPress={navigateToEditCategory} - shouldShowRightIcon - /> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + <OfflineWithFeedback pendingAction={policyCategory.pendingFields?.name}> + <MenuItemWithTopDescription + title={policyCategory.name} + description={translate(`workspace.categories.categoryName`)} + onPress={navigateToEditCategory} + shouldShowRightIcon + /> + </OfflineWithFeedback> + </View> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index b31207e73208..391021e7329d 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -9,9 +9,8 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -38,30 +37,28 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={CreateCategoryPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={CreateCategoryPage.displayName} - shouldEnableMaxHeight - > - <HeaderWithBackButton - title={translate('workspace.categories.addCategory')} - onBackButtonPress={Navigation.goBack} - /> - <CategoryForm - onSubmit={createCategory} - policyCategories={policyCategories} - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <HeaderWithBackButton + title={translate('workspace.categories.addCategory')} + onBackButtonPress={Navigation.goBack} + /> + <CategoryForm + onSubmit={createCategory} + policyCategories={policyCategories} + /> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx index dbf7c8913515..27e226f878c2 100644 --- a/src/pages/workspace/categories/EditCategoryPage.tsx +++ b/src/pages/workspace/categories/EditCategoryPage.tsx @@ -9,9 +9,8 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -60,32 +59,30 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) { ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={EditCategoryPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={EditCategoryPage.displayName} - shouldEnableMaxHeight - > - <HeaderWithBackButton - title={translate('workspace.categories.editCategory')} - onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))} - /> - <CategoryForm - onSubmit={editCategory} - validateEdit={validate} - categoryName={currentCategoryName} - policyCategories={policyCategories} - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <HeaderWithBackButton + title={translate('workspace.categories.editCategory')} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))} + /> + <CategoryForm + onSubmit={editCategory} + validateEdit={validate} + categoryName={currentCategoryName} + policyCategories={policyCategories} + /> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 80394623dba8..c7accdd79ed6 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -29,9 +29,8 @@ import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -248,73 +247,71 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat const shouldShowEmptyState = !categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) && !isLoading; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceCategoriesPage.displayName} + shouldShowOfflineIndicatorInWideScreen + offlineIndicatorStyle={styles.mtAuto} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceCategoriesPage.displayName} - shouldShowOfflineIndicatorInWideScreen - offlineIndicatorStyle={styles.mtAuto} + <HeaderWithBackButton + icon={Illustrations.FolderOpen} + title={translate('workspace.common.categories')} + shouldShowBackButton={isSmallScreenWidth} > - <HeaderWithBackButton - icon={Illustrations.FolderOpen} - title={translate('workspace.common.categories')} - shouldShowBackButton={isSmallScreenWidth} - > - {!isSmallScreenWidth && getHeaderButtons()} - </HeaderWithBackButton> - <ConfirmModal - isVisible={deleteCategoriesConfirmModalVisible} - onConfirm={handleDeleteCategories} - onCancel={() => setDeleteCategoriesConfirmModalVisible(false)} - title={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategory' : 'workspace.categories.deleteCategories')} - prompt={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategoryPrompt' : 'workspace.categories.deleteCategoriesPrompt')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + {!isSmallScreenWidth && getHeaderButtons()} + </HeaderWithBackButton> + <ConfirmModal + isVisible={deleteCategoriesConfirmModalVisible} + onConfirm={handleDeleteCategories} + onCancel={() => setDeleteCategoriesConfirmModalVisible(false)} + title={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategory' : 'workspace.categories.deleteCategories')} + prompt={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategoryPrompt' : 'workspace.categories.deleteCategoriesPrompt')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} + <View style={[styles.ph5, styles.pb5, styles.pt3]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.categories.subtitle')}</Text> + </View> + {isLoading && ( + <ActivityIndicator + size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} + style={[styles.flex1]} + color={theme.spinner} + /> + )} + {shouldShowEmptyState && ( + <WorkspaceEmptyStateSection + title={translate('workspace.categories.emptyCategories.title')} + icon={Illustrations.EmptyStateExpenses} + subtitle={translate('workspace.categories.emptyCategories.subtitle')} + /> + )} + {!shouldShowEmptyState && !isLoading && ( + <SelectionList + canSelectMultiple + sections={[{data: categoryList, isDisabled: false}]} + onCheckboxPress={toggleCategory} + onSelectRow={navigateToCategorySettings} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + onSelectAll={toggleAllCategories} + showScrollIndicator + ListItem={TableListItem} + onDismissError={dismissError} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> - {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} - <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.categories.subtitle')}</Text> - </View> - {isLoading && ( - <ActivityIndicator - size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} - style={[styles.flex1]} - color={theme.spinner} - /> - )} - {shouldShowEmptyState && ( - <WorkspaceEmptyStateSection - title={translate('workspace.categories.emptyCategories.title')} - icon={Illustrations.EmptyStateExpenses} - subtitle={translate('workspace.categories.emptyCategories.subtitle')} - /> - )} - {!shouldShowEmptyState && !isLoading && ( - <SelectionList - canSelectMultiple - sections={[{data: categoryList, isDisabled: false}]} - onCheckboxPress={toggleCategory} - onSelectRow={navigateToCategorySettings} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - onSelectAll={toggleAllCategories} - showScrollIndicator - ListItem={TableListItem} - onDismissError={dismissError} - customListHeader={getCustomListHeader()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - /> - )} - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + )} + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 0ebfcde6f97b..2a5af39a5337 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -13,9 +13,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceRequiresCategory} from '@libs/actions/Policy'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -38,43 +37,41 @@ function WorkspaceCategoriesSettingsPage({route, policyCategories}: WorkspaceCat const hasEnabledOptions = OptionsListUtils.hasEnabledOptions(policyCategories ?? {}); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} - > - {({policy}) => ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceCategoriesSettingsPage.displayName} - > - <HeaderWithBackButton title={translate('common.settings')} /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={policy?.errorFields?.requiresCategory} - pendingAction={policy?.pendingFields?.requiresCategory} - errorRowStyles={styles.mh5} - > - <View style={[styles.mt2, styles.mh4]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text style={[styles.textNormal, styles.colorMuted, styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.requiresCategory')}</Text> - <Switch - isOn={policy?.requiresCategory ?? false} - accessibilityLabel={translate('workspace.categories.requiresCategory')} - onToggle={updateWorkspaceRequiresCategory} - disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} - /> - </View> + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + {({policy}) => ( + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceCategoriesSettingsPage.displayName} + > + <HeaderWithBackButton title={translate('common.settings')} /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={policy?.errorFields?.requiresCategory} + pendingAction={policy?.pendingFields?.requiresCategory} + errorRowStyles={styles.mh5} + > + <View style={[styles.mt2, styles.mh4]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text style={[styles.textNormal, styles.colorMuted, styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.requiresCategory')}</Text> + <Switch + isOn={policy?.requiresCategory ?? false} + accessibilityLabel={translate('workspace.categories.requiresCategory')} + onToggle={updateWorkspaceRequiresCategory} + disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} + /> </View> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - )} - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + </View> + </ScreenWrapper> + )} + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx index 1d01856992f9..228a64d4a2ee 100644 --- a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx +++ b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx @@ -14,9 +14,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getOptimisticRateName, validateRateValue} from '@libs/PolicyDistanceRatesUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import {createPolicyDistanceRate, generateCustomUnitID} from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -60,44 +59,42 @@ function CreateDistanceRatePage({policy, route}: CreateDistanceRatePageProps) { }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={CreateDistanceRatePage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={CreateDistanceRatePage.displayName} - shouldEnableMaxHeight + <HeaderWithBackButton title={translate('workspace.distanceRates.addRate')} /> + <FormProvider + formID={ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM} + submitButtonText={translate('common.save')} + onSubmit={submit} + validate={validate} + enabledWhenOffline + style={[styles.flexGrow1]} + shouldHideFixErrorsAlert + submitFlexEnabled={false} + submitButtonStyles={[styles.mh5, styles.mt0]} + disablePressOnEnter={false} > - <HeaderWithBackButton title={translate('workspace.distanceRates.addRate')} /> - <FormProvider - formID={ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM} - submitButtonText={translate('common.save')} - onSubmit={submit} - validate={validate} - enabledWhenOffline - style={[styles.flexGrow1]} - shouldHideFixErrorsAlert - submitFlexEnabled={false} - submitButtonStyles={[styles.mh5, styles.mt0]} - disablePressOnEnter={false} - > - <InputWrapperWithRef - InputComponent={AmountForm} - inputID={INPUT_IDS.RATE} - extraDecimals={1} - isCurrencyPressable={false} - currency={currency} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <InputWrapperWithRef + InputComponent={AmountForm} + inputID={INPUT_IDS.RATE} + extraDecimals={1} + isCurrencyPressable={false} + currency={currency} + ref={inputCallbackRef} + /> + </FormProvider> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx index a1716405da8b..7e215054b1b6 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx @@ -18,9 +18,8 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -93,77 +92,75 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + testID={PolicyDistanceRateDetailsPage.displayName} + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - testID={PolicyDistanceRateDetailsPage.displayName} - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - shouldShowOfflineIndicatorInWideScreen - > - <HeaderWithBackButton - title={`${rateValueToDisplay} / ${translate(`common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`)}`} - shouldShowThreeDotsButton - threeDotsMenuItems={threeDotsMenuItems} - threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} - /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(rate, 'enabled')} - pendingAction={rate?.pendingFields?.enabled} - errorRowStyles={styles.mh5} - onClose={() => clearErrorFields('enabled')} - > - <View style={[styles.flexRow, styles.justifyContentBetween, styles.p5]}> - <Text>{translate('workspace.distanceRates.enableRate')}</Text> - <Switch - isOn={rate.enabled ?? false} - onToggle={toggleRate} - accessibilityLabel={translate('workspace.distanceRates.enableRate')} - /> - </View> - </OfflineWithFeedback> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(rate, 'rate')} - pendingAction={rate?.pendingFields?.rate ?? rate?.pendingFields?.currency} - errorRowStyles={styles.mh5} - onClose={() => clearErrorFields('rate')} - > - <MenuItemWithTopDescription - shouldShowRightIcon - title={`${rateValueToDisplay} / ${unitToDisplay}`} - description={translate('workspace.distanceRates.rate')} - descriptionTextStyle={styles.textNormal} - onPress={editRateValue} + <HeaderWithBackButton + title={`${rateValueToDisplay} / ${translate(`common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`)}`} + shouldShowThreeDotsButton + threeDotsMenuItems={threeDotsMenuItems} + threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} + /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(rate, 'enabled')} + pendingAction={rate?.pendingFields?.enabled} + errorRowStyles={styles.mh5} + onClose={() => clearErrorFields('enabled')} + > + <View style={[styles.flexRow, styles.justifyContentBetween, styles.p5]}> + <Text>{translate('workspace.distanceRates.enableRate')}</Text> + <Switch + isOn={rate.enabled ?? false} + onToggle={toggleRate} + accessibilityLabel={translate('workspace.distanceRates.enableRate')} /> - </OfflineWithFeedback> - <ConfirmModal - onConfirm={() => setIsWarningModalVisible(false)} - isVisible={isWarningModalVisible} - title={translate('workspace.distanceRates.oopsNotSoFast')} - prompt={translate('workspace.distanceRates.workspaceNeeds')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} - /> - <ConfirmModal - title={translate('workspace.distanceRates.deleteDistanceRate')} - isVisible={isDeleteModalVisible} - onConfirm={deleteRate} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={translate('workspace.distanceRates.areYouSureDelete', {count: 1})} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + </View> + </OfflineWithFeedback> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(rate, 'rate')} + pendingAction={rate?.pendingFields?.rate ?? rate?.pendingFields?.currency} + errorRowStyles={styles.mh5} + onClose={() => clearErrorFields('rate')} + > + <MenuItemWithTopDescription + shouldShowRightIcon + title={`${rateValueToDisplay} / ${unitToDisplay}`} + description={translate('workspace.distanceRates.rate')} + descriptionTextStyle={styles.textNormal} + onPress={editRateValue} /> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </OfflineWithFeedback> + <ConfirmModal + onConfirm={() => setIsWarningModalVisible(false)} + isVisible={isWarningModalVisible} + title={translate('workspace.distanceRates.oopsNotSoFast')} + prompt={translate('workspace.distanceRates.workspaceNeeds')} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} + /> + <ConfirmModal + title={translate('workspace.distanceRates.deleteDistanceRate')} + isVisible={isDeleteModalVisible} + onConfirm={deleteRate} + onCancel={() => setIsDeleteModalVisible(false)} + prompt={translate('workspace.distanceRates.areYouSureDelete', {count: 1})} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </View> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx index f44175f09e3f..805a0b4b111b 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx @@ -15,9 +15,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {validateRateValue} from '@libs/PolicyDistanceRatesUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -57,49 +56,47 @@ function PolicyDistanceRateEditPage({policy, route}: PolicyDistanceRateEditPageP ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={PolicyDistanceRateEditPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={PolicyDistanceRateEditPage.displayName} - shouldEnableMaxHeight + <HeaderWithBackButton + title={translate('workspace.distanceRates.rate')} + shouldShowBackButton + onBackButtonPress={() => Navigation.goBack()} + /> + <FormProvider + formID={ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM} + submitButtonText={translate('common.save')} + onSubmit={submitRate} + validate={validate} + enabledWhenOffline + style={[styles.flexGrow1]} + shouldHideFixErrorsAlert + submitFlexEnabled={false} + submitButtonStyles={[styles.mh5, styles.mt0]} + disablePressOnEnter={false} > - <HeaderWithBackButton - title={translate('workspace.distanceRates.rate')} - shouldShowBackButton - onBackButtonPress={() => Navigation.goBack()} + <InputWrapperWithRef + InputComponent={AmountForm} + inputID={INPUT_IDS.RATE} + extraDecimals={1} + defaultValue={(parseFloat(currentRateValue) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET).toFixed(3)} + isCurrencyPressable={false} + currency={currency} + ref={inputCallbackRef} /> - <FormProvider - formID={ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM} - submitButtonText={translate('common.save')} - onSubmit={submitRate} - validate={validate} - enabledWhenOffline - style={[styles.flexGrow1]} - shouldHideFixErrorsAlert - submitFlexEnabled={false} - submitButtonStyles={[styles.mh5, styles.mt0]} - disablePressOnEnter={false} - > - <InputWrapperWithRef - InputComponent={AmountForm} - inputID={INPUT_IDS.RATE} - extraDecimals={1} - defaultValue={(parseFloat(currentRateValue) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET).toFixed(3)} - isCurrencyPressable={false} - currency={currency} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </FormProvider> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index ea35526bcdfa..768bfdadcb8a 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -25,9 +25,8 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu'; import CONST from '@src/CONST'; @@ -266,73 +265,71 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={PolicyDistanceRatesPage.displayName} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={PolicyDistanceRatesPage.displayName} - shouldShowOfflineIndicatorInWideScreen + <HeaderWithBackButton + icon={Illustrations.CarIce} + title={translate('workspace.common.distanceRates')} + shouldShowBackButton={isSmallScreenWidth} > - <HeaderWithBackButton - icon={Illustrations.CarIce} - title={translate('workspace.common.distanceRates')} - shouldShowBackButton={isSmallScreenWidth} - > - {!isSmallScreenWidth && headerButtons} - </HeaderWithBackButton> - {isSmallScreenWidth && <View style={[styles.ph5]}>{headerButtons}</View>} - <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.distanceRates.centrallyManage')}</Text> - </View> - {isLoading && ( - <ActivityIndicator - size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} - style={[styles.flex1]} - color={theme.spinner} - /> - )} - {Object.values(customUnitRates).length > 0 && ( - <SelectionList - canSelectMultiple - sections={[{data: distanceRatesList, isDisabled: false}]} - onCheckboxPress={toggleRate} - onSelectRow={openRateDetails} - onSelectAll={toggleAllRates} - onDismissError={dismissError} - showScrollIndicator - ListItem={TableListItem} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - customListHeader={getCustomListHeader()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - /> - )} - <ConfirmModal - onConfirm={() => setIsWarningModalVisible(false)} - isVisible={isWarningModalVisible} - title={translate('workspace.distanceRates.oopsNotSoFast')} - prompt={translate('workspace.distanceRates.workspaceNeeds')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} + {!isSmallScreenWidth && headerButtons} + </HeaderWithBackButton> + {isSmallScreenWidth && <View style={[styles.ph5]}>{headerButtons}</View>} + <View style={[styles.ph5, styles.pb5, styles.pt3]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.distanceRates.centrallyManage')}</Text> + </View> + {isLoading && ( + <ActivityIndicator + size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} + style={[styles.flex1]} + color={theme.spinner} /> - <ConfirmModal - title={translate('workspace.distanceRates.deleteDistanceRate')} - isVisible={isDeleteModalVisible} - onConfirm={deleteRates} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={translate('workspace.distanceRates.areYouSureDelete', {count: selectedDistanceRates.length})} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + )} + {Object.values(customUnitRates).length > 0 && ( + <SelectionList + canSelectMultiple + sections={[{data: distanceRatesList, isDisabled: false}]} + onCheckboxPress={toggleRate} + onSelectRow={openRateDetails} + onSelectAll={toggleAllRates} + onDismissError={dismissError} + showScrollIndicator + ListItem={TableListItem} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + )} + <ConfirmModal + onConfirm={() => setIsWarningModalVisible(false)} + isVisible={isWarningModalVisible} + title={translate('workspace.distanceRates.oopsNotSoFast')} + prompt={translate('workspace.distanceRates.workspaceNeeds')} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} + /> + <ConfirmModal + title={translate('workspace.distanceRates.deleteDistanceRate')} + isVisible={isDeleteModalVisible} + onConfirm={deleteRates} + onCancel={() => setIsDeleteModalVisible(false)} + prompt={translate('workspace.distanceRates.areYouSureDelete', {count: selectedDistanceRates.length})} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index 0cf5c133e37f..d99b15bb857e 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -13,9 +13,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -68,53 +67,51 @@ function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: Poli }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={PolicyDistanceRatesSettingsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={PolicyDistanceRatesSettingsPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.common.settings')} /> - <View style={styles.flexGrow1}> + <HeaderWithBackButton title={translate('workspace.common.settings')} /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(customUnits[customUnitID], 'attributes')} + pendingAction={customUnits[customUnitID].pendingFields?.attributes} + errorRowStyles={styles.mh5} + onClose={() => clearErrorFields('attributes')} + > + <UnitSelector + label={translate('workspace.distanceRates.unit')} + defaultValue={defaultUnit} + wrapperStyle={[styles.ph5, styles.mt3]} + setNewUnit={setNewUnit} + /> + </OfflineWithFeedback> + {policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && ( <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(customUnits[customUnitID], 'attributes')} - pendingAction={customUnits[customUnitID].pendingFields?.attributes} + errors={ErrorUtils.getLatestErrorField(customUnits[customUnitID], 'defaultCategory')} + pendingAction={customUnits[customUnitID].pendingFields?.defaultCategory} errorRowStyles={styles.mh5} - onClose={() => clearErrorFields('attributes')} + onClose={() => clearErrorFields('defaultCategory')} > - <UnitSelector - label={translate('workspace.distanceRates.unit')} - defaultValue={defaultUnit} + <CategorySelector + policyID={policyID} + label={translate('workspace.distanceRates.defaultCategory')} + defaultValue={defaultCategory} wrapperStyle={[styles.ph5, styles.mt3]} - setNewUnit={setNewUnit} + setNewCategory={setNewCategory} /> </OfflineWithFeedback> - {policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && ( - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(customUnits[customUnitID], 'defaultCategory')} - pendingAction={customUnits[customUnitID].pendingFields?.defaultCategory} - errorRowStyles={styles.mh5} - onClose={() => clearErrorFields('defaultCategory')} - > - <CategorySelector - policyID={policyID} - label={translate('workspace.distanceRates.defaultCategory')} - defaultValue={defaultCategory} - wrapperStyle={[styles.ph5, styles.mt3]} - setNewCategory={setNewCategory} - /> - </OfflineWithFeedback> - )} - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + )} + </View> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 32b43a230619..d9151e87553e 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -21,8 +21,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as UserUtils from '@libs/UserUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import * as Policy from '@userActions/Policy'; @@ -129,89 +128,87 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM }, [accountID, policyID]); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <ScreenWrapper testID={WorkspaceMemberDetailsPage.displayName}> - <HeaderWithBackButton - title={displayName} - subtitle={policy?.name} - /> - <View style={[styles.containerWithSpaceBetween, styles.pointerEventsBoxNone, styles.justifyContentStart]}> - <View style={[styles.avatarSectionWrapper, styles.pb0]}> - <OfflineWithFeedback pendingAction={details.pendingFields?.avatar}> - <Avatar - containerStyles={[styles.avatarXLarge, styles.mv5, styles.noOutline]} - imageStyles={[styles.avatarXLarge]} - source={UserUtils.getAvatar(avatar, accountID)} - size={CONST.AVATAR_SIZE.XLARGE} - fallbackIcon={fallbackIcon} - /> - </OfflineWithFeedback> - {Boolean(details.displayName ?? '') && ( - <Text - style={[styles.textHeadline, styles.pre, styles.mb6, styles.w100, styles.textAlignCenter]} - numberOfLines={1} - > - {displayName} - </Text> - )} - {isSelectedMemberOwner && isCurrentUserAdmin && !isCurrentUserOwner ? ( - <Button - text={translate('workspace.people.transferOwner')} - onPress={startChangeOwnershipFlow} - medium - isDisabled={isOffline} - icon={Expensicons.Transfer} - iconStyles={StyleUtils.getTransformScaleStyle(0.8)} - style={styles.mv5} - /> - ) : ( - <Button - text={translate('workspace.people.removeMemberButtonTitle')} - onPress={askForConfirmationToRemove} - medium - isDisabled={isSelectedMemberOwner || isSelectedMemberCurrentUser} - icon={Expensicons.RemoveMembers} - iconStyles={StyleUtils.getTransformScaleStyle(0.8)} - style={styles.mv5} - /> - )} - <ConfirmModal - danger - title={translate('workspace.people.removeMemberTitle')} - isVisible={isRemoveMemberConfirmModalVisible} - onConfirm={removeUser} - onCancel={() => setIsRemoveMemberConfirmModalVisible(false)} - prompt={translate('workspace.people.removeMemberPrompt', {memberName: displayName})} - confirmText={translate('common.remove')} - cancelText={translate('common.cancel')} + <AccessOrNotFoundWrapper policyID={policyID}> + <ScreenWrapper testID={WorkspaceMemberDetailsPage.displayName}> + <HeaderWithBackButton + title={displayName} + subtitle={policy?.name} + /> + <View style={[styles.containerWithSpaceBetween, styles.pointerEventsBoxNone, styles.justifyContentStart]}> + <View style={[styles.avatarSectionWrapper, styles.pb0]}> + <OfflineWithFeedback pendingAction={details.pendingFields?.avatar}> + <Avatar + containerStyles={[styles.avatarXLarge, styles.mv5, styles.noOutline]} + imageStyles={[styles.avatarXLarge]} + source={UserUtils.getAvatar(avatar, accountID)} + size={CONST.AVATAR_SIZE.XLARGE} + fallbackIcon={fallbackIcon} /> - </View> - <View style={styles.w100}> - <MenuItemWithTopDescription - disabled={isSelectedMemberOwner || isSelectedMemberCurrentUser} - title={member?.role === CONST.POLICY.ROLE.ADMIN ? translate('common.admin') : translate('common.member')} - description={translate('common.role')} - shouldShowRightIcon - onPress={openRoleSelectionModal} + </OfflineWithFeedback> + {Boolean(details.displayName ?? '') && ( + <Text + style={[styles.textHeadline, styles.pre, styles.mb6, styles.w100, styles.textAlignCenter]} + numberOfLines={1} + > + {displayName} + </Text> + )} + {isSelectedMemberOwner && isCurrentUserAdmin && !isCurrentUserOwner ? ( + <Button + text={translate('workspace.people.transferOwner')} + onPress={startChangeOwnershipFlow} + medium + isDisabled={isOffline} + icon={Expensicons.Transfer} + iconStyles={StyleUtils.getTransformScaleStyle(0.8)} + style={styles.mv5} /> - <MenuItem - title={translate('common.profile')} - icon={Expensicons.Info} - onPress={navigateToProfile} - shouldShowRightIcon + ) : ( + <Button + text={translate('workspace.people.removeMemberButtonTitle')} + onPress={askForConfirmationToRemove} + medium + isDisabled={isSelectedMemberOwner || isSelectedMemberCurrentUser} + icon={Expensicons.RemoveMembers} + iconStyles={StyleUtils.getTransformScaleStyle(0.8)} + style={styles.mv5} /> - <WorkspaceMemberDetailsRoleSelectionModal - isVisible={isRoleSelectionModalVisible} - items={roleItems} - onRoleChange={changeRole} - onClose={() => setIsRoleSelectionModalVisible(false)} - /> - </View> + )} + <ConfirmModal + danger + title={translate('workspace.people.removeMemberTitle')} + isVisible={isRemoveMemberConfirmModalVisible} + onConfirm={removeUser} + onCancel={() => setIsRemoveMemberConfirmModalVisible(false)} + prompt={translate('workspace.people.removeMemberPrompt', {memberName: displayName})} + confirmText={translate('common.remove')} + cancelText={translate('common.cancel')} + /> + </View> + <View style={styles.w100}> + <MenuItemWithTopDescription + disabled={isSelectedMemberOwner || isSelectedMemberCurrentUser} + title={member?.role === CONST.POLICY.ROLE.ADMIN ? translate('common.admin') : translate('common.member')} + description={translate('common.role')} + shouldShowRightIcon + onPress={openRoleSelectionModal} + /> + <MenuItem + title={translate('common.profile')} + icon={Expensicons.Info} + onPress={navigateToProfile} + shouldShowRightIcon + /> + <WorkspaceMemberDetailsRoleSelectionModal + isVisible={isRoleSelectionModalVisible} + items={roleItems} + onRoleChange={changeRole} + onClose={() => setIsRoleSelectionModalVisible(false)} + /> </View> - </ScreenWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </View> + </ScreenWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx index 8604ba23fc92..55870d9f4d7a 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx @@ -13,8 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as PolicyActions from '@userActions/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -36,41 +35,39 @@ function WorkspaceOwnerChangeErrorPage({route}: WorkspaceOwnerChangeSuccessPageP }, [accountID, policyID]); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <ScreenWrapper testID={WorkspaceOwnerChangeErrorPage.displayName}> - <HeaderWithBackButton - title={translate('workspace.changeOwner.changeOwnerPageTitle')} - onBackButtonPress={closePage} + <AccessOrNotFoundWrapper policyID={policyID}> + <ScreenWrapper testID={WorkspaceOwnerChangeErrorPage.displayName}> + <HeaderWithBackButton + title={translate('workspace.changeOwner.changeOwnerPageTitle')} + onBackButtonPress={closePage} + /> + <View style={[styles.screenCenteredContainer, styles.alignItemsCenter]}> + <Icon + src={Expensicons.MoneyWaving} + width={187} + height={173} + fill="" + additionalStyles={styles.mb3} /> - <View style={[styles.screenCenteredContainer, styles.alignItemsCenter]}> - <Icon - src={Expensicons.MoneyWaving} - width={187} - height={173} - fill="" - additionalStyles={styles.mb3} - /> - <Text style={[styles.textHeadline, styles.textAlignCenter, styles.mv2]}>{translate('workspace.changeOwner.errorTitle')}</Text> - <Text style={[styles.textAlignCenter, styles.textSupporting]}> - {translate('workspace.changeOwner.errorDescriptionPartOne')}{' '} - <TextLink href={`mailto:${CONST.EMAIL.CONCIERGE}`}>{translate('workspace.changeOwner.errorDescriptionPartTwo')}</TextLink>{' '} - {translate('workspace.changeOwner.errorDescriptionPartThree')} - </Text> - </View> - <FixedFooter> - <Button - success - large - text={translate('common.buttonConfirm')} - style={styles.mt6} - pressOnEnter - onPress={closePage} - /> - </FixedFooter> - </ScreenWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <Text style={[styles.textHeadline, styles.textAlignCenter, styles.mv2]}>{translate('workspace.changeOwner.errorTitle')}</Text> + <Text style={[styles.textAlignCenter, styles.textSupporting]}> + {translate('workspace.changeOwner.errorDescriptionPartOne')}{' '} + <TextLink href={`mailto:${CONST.EMAIL.CONCIERGE}`}>{translate('workspace.changeOwner.errorDescriptionPartTwo')}</TextLink>{' '} + {translate('workspace.changeOwner.errorDescriptionPartThree')} + </Text> + </View> + <FixedFooter> + <Button + success + large + text={translate('common.buttonConfirm')} + style={styles.mt6} + pressOnEnter + onPress={closePage} + /> + </FixedFooter> + </ScreenWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx index c4968128df06..6218ef1208ad 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx @@ -8,8 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as PolicyActions from '@userActions/Policy'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -30,25 +29,23 @@ function WorkspaceOwnerChangeSuccessPage({route}: WorkspaceOwnerChangeSuccessPag }, [accountID, policyID]); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <ScreenWrapper testID={WorkspaceOwnerChangeSuccessPage.displayName}> - <HeaderWithBackButton - title={translate('workspace.changeOwner.changeOwnerPageTitle')} - onBackButtonPress={closePage} - /> - <ConfirmationPage - animation={LottieAnimations.Fireworks} - heading={translate('workspace.changeOwner.successTitle')} - description={translate('workspace.changeOwner.successDescription')} - descriptionStyle={styles.textSupporting} - shouldShowButton - buttonText={translate('common.buttonConfirm')} - onButtonPress={closePage} - /> - </ScreenWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <AccessOrNotFoundWrapper policyID={policyID}> + <ScreenWrapper testID={WorkspaceOwnerChangeSuccessPage.displayName}> + <HeaderWithBackButton + title={translate('workspace.changeOwner.changeOwnerPageTitle')} + onBackButtonPress={closePage} + /> + <ConfirmationPage + animation={LottieAnimations.Fireworks} + heading={translate('workspace.changeOwner.successTitle')} + description={translate('workspace.changeOwner.successDescription')} + descriptionStyle={styles.textSupporting} + shouldShowButton + buttonText={translate('common.buttonConfirm')} + onButtonPress={closePage} + /> + </ScreenWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx index 6eda891a8fc9..75106a6a9fe1 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx @@ -9,8 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import * as PolicyActions from '@userActions/Policy'; @@ -55,33 +54,31 @@ function WorkspaceOwnerChangeWrapperPage({route, policy}: WorkspaceOwnerChangeWr }, [accountID, policy, policy?.errorFields?.changeOwner, policyID]); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <ScreenWrapper testID={WorkspaceOwnerChangeWrapperPage.displayName}> - <HeaderWithBackButton - title={translate('workspace.changeOwner.changeOwnerPageTitle')} - onBackButtonPress={() => { - PolicyActions.clearWorkspaceOwnerChangeFlow(policyID); - Navigation.goBack(); - Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID)); - }} - /> - <View style={[styles.containerWithSpaceBetween, styles.ph5, error === CONST.POLICY.OWNERSHIP_ERRORS.NO_BILLING_CARD ? styles.pb0 : styles.pb5]}> - {policy?.isLoading && <FullScreenLoadingIndicator />} - {!policy?.isLoading && - (error === CONST.POLICY.OWNERSHIP_ERRORS.NO_BILLING_CARD ? ( - <WorkspaceOwnerPaymentCardForm policy={policy} /> - ) : ( - <WorkspaceOwnerChangeCheck - policy={policy} - accountID={accountID} - error={error} - /> - ))} - </View> - </ScreenWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <AccessOrNotFoundWrapper policyID={policyID}> + <ScreenWrapper testID={WorkspaceOwnerChangeWrapperPage.displayName}> + <HeaderWithBackButton + title={translate('workspace.changeOwner.changeOwnerPageTitle')} + onBackButtonPress={() => { + PolicyActions.clearWorkspaceOwnerChangeFlow(policyID); + Navigation.goBack(); + Navigation.navigate(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID)); + }} + /> + <View style={[styles.containerWithSpaceBetween, styles.ph5, error === CONST.POLICY.OWNERSHIP_ERRORS.NO_BILLING_CARD ? styles.pb0 : styles.pb5]}> + {policy?.isLoading && <FullScreenLoadingIndicator />} + {!policy?.isLoading && + (error === CONST.POLICY.OWNERSHIP_ERRORS.NO_BILLING_CARD ? ( + <WorkspaceOwnerPaymentCardForm policy={policy} /> + ) : ( + <WorkspaceOwnerChangeCheck + policy={policy} + accountID={accountID} + error={error} + /> + ))} + </View> + </ScreenWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index d329d74dbca0..04656dc44193 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -16,9 +16,8 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -69,45 +68,43 @@ function EditTagPage({route, policyTags}: EditTagPageProps) { ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={EditTagPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={EditTagPage.displayName} - shouldEnableMaxHeight + <HeaderWithBackButton + title={translate('workspace.tags.editTag')} + onBackButtonPress={Navigation.goBack} + /> + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAG_FORM} + onSubmit={editTag} + submitButtonText={translate('common.save')} + validate={validate} + style={[styles.mh5, styles.flex1]} + enabledWhenOffline > - <HeaderWithBackButton - title={translate('workspace.tags.editTag')} - onBackButtonPress={Navigation.goBack} + <InputWrapper + InputComponent={TextInput} + maxLength={CONST.TAG_NAME_LIMIT} + defaultValue={currentTagName} + label={translate('common.name')} + accessibilityLabel={translate('common.name')} + inputID={INPUT_IDS.TAG_NAME} + role={CONST.ROLE.PRESENTATION} + ref={inputCallbackRef} /> - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAG_FORM} - onSubmit={editTag} - submitButtonText={translate('common.save')} - validate={validate} - style={[styles.mh5, styles.flex1]} - enabledWhenOffline - > - <InputWrapper - InputComponent={TextInput} - maxLength={CONST.TAG_NAME_LIMIT} - defaultValue={currentTagName} - label={translate('common.name')} - accessibilityLabel={translate('common.name')} - inputID={INPUT_IDS.TAG_NAME} - role={CONST.ROLE.PRESENTATION} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </FormProvider> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index 3e0b8eae30ee..827487666fb4 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -20,9 +20,8 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -67,72 +66,70 @@ function TagSettingsPage({route, policyTags}: TagSettingsPageProps) { }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={TagSettingsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={TagSettingsPage.displayName} - > - <HeaderWithBackButton - title={PolicyUtils.getCleanedTagName(route.params.tagName)} - shouldShowThreeDotsButton - shouldSetModalVisibility={false} - threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} - threeDotsMenuItems={[ - { - icon: Trashcan, - text: translate('workspace.tags.deleteTag'), - onSelected: () => setIsDeleteTagModalOpen(true), - }, - ]} - /> - <ConfirmModal - title={translate('workspace.tags.deleteTag')} - isVisible={isDeleteTagModalOpen} - onConfirm={deleteTagAndHideModal} - onCancel={() => setIsDeleteTagModalOpen(false)} - shouldSetModalVisibility={false} - prompt={translate('workspace.tags.deleteTagConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorMessageField(currentPolicyTag)} - pendingAction={currentPolicyTag.pendingFields?.enabled} - errorRowStyles={styles.mh5} - onClose={() => Policy.clearPolicyTagErrors(route.params.policyID, route.params.tagName)} - > - <View style={[styles.mt2, styles.mh5]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text>{translate('workspace.tags.enableTag')}</Text> - <Switch - isOn={currentPolicyTag.enabled} - accessibilityLabel={translate('workspace.tags.enableTag')} - onToggle={updateWorkspaceTagEnabled} - /> - </View> + <HeaderWithBackButton + title={PolicyUtils.getCleanedTagName(route.params.tagName)} + shouldShowThreeDotsButton + shouldSetModalVisibility={false} + threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} + threeDotsMenuItems={[ + { + icon: Trashcan, + text: translate('workspace.tags.deleteTag'), + onSelected: () => setIsDeleteTagModalOpen(true), + }, + ]} + /> + <ConfirmModal + title={translate('workspace.tags.deleteTag')} + isVisible={isDeleteTagModalOpen} + onConfirm={deleteTagAndHideModal} + onCancel={() => setIsDeleteTagModalOpen(false)} + shouldSetModalVisibility={false} + prompt={translate('workspace.tags.deleteTagConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorMessageField(currentPolicyTag)} + pendingAction={currentPolicyTag.pendingFields?.enabled} + errorRowStyles={styles.mh5} + onClose={() => Policy.clearPolicyTagErrors(route.params.policyID, route.params.tagName)} + > + <View style={[styles.mt2, styles.mh5]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text>{translate('workspace.tags.enableTag')}</Text> + <Switch + isOn={currentPolicyTag.enabled} + accessibilityLabel={translate('workspace.tags.enableTag')} + onToggle={updateWorkspaceTagEnabled} + /> </View> - </OfflineWithFeedback> - <OfflineWithFeedback pendingAction={currentPolicyTag.pendingFields?.name}> - <MenuItemWithTopDescription - title={PolicyUtils.getCleanedTagName(currentPolicyTag.name)} - description={translate(`workspace.tags.tagName`)} - onPress={navigateToEditTag} - shouldShowRightIcon - /> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + <OfflineWithFeedback pendingAction={currentPolicyTag.pendingFields?.name}> + <MenuItemWithTopDescription + title={PolicyUtils.getCleanedTagName(currentPolicyTag.name)} + description={translate(`workspace.tags.tagName`)} + onPress={navigateToEditTag} + shouldShowRightIcon + /> + </OfflineWithFeedback> + </View> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx index 346d56891dd5..1c4718d93d82 100644 --- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx @@ -17,9 +17,8 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -69,44 +68,42 @@ function CreateTagPage({route, policyTags}: CreateTagPageProps) { ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={CreateTagPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={CreateTagPage.displayName} - shouldEnableMaxHeight + <HeaderWithBackButton + title={translate('workspace.tags.addTag')} + onBackButtonPress={Navigation.goBack} + /> + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAG_FORM} + onSubmit={createTag} + submitButtonText={translate('common.save')} + validate={validate} + style={[styles.mh5, styles.flex1]} + enabledWhenOffline > - <HeaderWithBackButton - title={translate('workspace.tags.addTag')} - onBackButtonPress={Navigation.goBack} + <InputWrapper + InputComponent={TextInput} + maxLength={CONST.TAG_NAME_LIMIT} + label={translate('common.name')} + accessibilityLabel={translate('common.name')} + inputID={INPUT_IDS.TAG_NAME} + role={CONST.ROLE.PRESENTATION} + ref={inputCallbackRef} /> - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAG_FORM} - onSubmit={createTag} - submitButtonText={translate('common.save')} - validate={validate} - style={[styles.mh5, styles.flex1]} - enabledWhenOffline - > - <InputWrapper - InputComponent={TextInput} - maxLength={CONST.TAG_NAME_LIMIT} - label={translate('common.name')} - accessibilityLabel={translate('common.name')} - inputID={INPUT_IDS.TAG_NAME} - role={CONST.ROLE.PRESENTATION} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </FormProvider> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index ffee71bd5011..202d8bacbbd1 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -16,9 +16,8 @@ import * as Policy from '@libs/actions/Policy'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -57,42 +56,40 @@ function WorkspaceEditTagsPage({route, policyTags}: WorkspaceEditTagsPageProps) ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceEditTagsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceEditTagsPage.displayName} + <HeaderWithBackButton title={translate(`workspace.tags.customTagName`)} /> + <FormProvider + style={[styles.flexGrow1, styles.ph5]} + formID={ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM} + onSubmit={updateTaglistName} + validate={validateTagName} + submitButtonText={translate('common.save')} + enabledWhenOffline > - <HeaderWithBackButton title={translate(`workspace.tags.customTagName`)} /> - <FormProvider - style={[styles.flexGrow1, styles.ph5]} - formID={ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM} - onSubmit={updateTaglistName} - validate={validateTagName} - submitButtonText={translate('common.save')} - enabledWhenOffline - > - <View style={styles.mb4}> - <InputWrapper - InputComponent={TextInput} - inputID={INPUT_IDS.POLICY_TAGS_NAME} - label={translate(`workspace.tags.customTagName`)} - accessibilityLabel={translate(`workspace.tags.customTagName`)} - defaultValue={PolicyUtils.getCleanedTagName(taglistName)} - role={CONST.ROLE.PRESENTATION} - ref={inputCallbackRef} - /> - </View> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <View style={styles.mb4}> + <InputWrapper + InputComponent={TextInput} + inputID={INPUT_IDS.POLICY_TAGS_NAME} + label={translate(`workspace.tags.customTagName`)} + accessibilityLabel={translate(`workspace.tags.customTagName`)} + defaultValue={PolicyUtils.getCleanedTagName(taglistName)} + role={CONST.ROLE.PRESENTATION} + ref={inputCallbackRef} + /> + </View> + </FormProvider> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index fafee7b3b74d..66ed33ad8237 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -29,9 +29,8 @@ import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -257,73 +256,71 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceTagsPage.displayName} + shouldShowOfflineIndicatorInWideScreen + offlineIndicatorStyle={styles.mtAuto} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceTagsPage.displayName} - shouldShowOfflineIndicatorInWideScreen - offlineIndicatorStyle={styles.mtAuto} + <HeaderWithBackButton + icon={Illustrations.Tag} + title={translate('workspace.common.tags')} + shouldShowBackButton={isSmallScreenWidth} > - <HeaderWithBackButton - icon={Illustrations.Tag} - title={translate('workspace.common.tags')} - shouldShowBackButton={isSmallScreenWidth} - > - {!isSmallScreenWidth && getHeaderButtons()} - </HeaderWithBackButton> - <ConfirmModal - isVisible={deleteTagsConfirmModalVisible} - onConfirm={handleDeleteTags} - onCancel={() => setDeleteTagsConfirmModalVisible(false)} - title={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags')} - prompt={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTagConfirmation' : 'workspace.tags.deleteTagsConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + {!isSmallScreenWidth && getHeaderButtons()} + </HeaderWithBackButton> + <ConfirmModal + isVisible={deleteTagsConfirmModalVisible} + onConfirm={handleDeleteTags} + onCancel={() => setDeleteTagsConfirmModalVisible(false)} + title={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags')} + prompt={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTagConfirmation' : 'workspace.tags.deleteTagsConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} + <View style={[styles.ph5, styles.pb5, styles.pt3]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.subtitle')}</Text> + </View> + {isLoading && ( + <ActivityIndicator + size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} + style={[styles.flex1]} + color={theme.spinner} + /> + )} + {tagList.length === 0 && !isLoading && ( + <WorkspaceEmptyStateSection + title={translate('workspace.tags.emptyTags.title')} + icon={Illustrations.EmptyStateExpenses} + subtitle={translate('workspace.tags.emptyTags.subtitle')} + /> + )} + {tagList.length > 0 && !isLoading && ( + <SelectionList + canSelectMultiple + sections={[{data: tagList, isDisabled: false}]} + onCheckboxPress={toggleTag} + onSelectRow={navigateToTagSettings} + onSelectAll={toggleAllTags} + showScrollIndicator + ListItem={TableListItem} + customListHeader={getCustomListHeader()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + onDismissError={(item) => Policy.clearPolicyTagErrors(route.params.policyID, item.value)} /> - {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} - <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.subtitle')}</Text> - </View> - {isLoading && ( - <ActivityIndicator - size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} - style={[styles.flex1]} - color={theme.spinner} - /> - )} - {tagList.length === 0 && !isLoading && ( - <WorkspaceEmptyStateSection - title={translate('workspace.tags.emptyTags.title')} - icon={Illustrations.EmptyStateExpenses} - subtitle={translate('workspace.tags.emptyTags.subtitle')} - /> - )} - {tagList.length > 0 && !isLoading && ( - <SelectionList - canSelectMultiple - sections={[{data: tagList, isDisabled: false}]} - onCheckboxPress={toggleTag} - onSelectRow={navigateToTagSettings} - onSelectAll={toggleAllTags} - showScrollIndicator - ListItem={TableListItem} - customListHeader={getCustomListHeader()} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - onDismissError={(item) => Policy.clearPolicyTagErrors(route.params.policyID, item.value)} - /> - )} - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + )} + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index b421698b8f2f..e82e11588ebb 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -15,9 +15,8 @@ import * as Policy from '@libs/actions/Policy'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -42,54 +41,52 @@ function WorkspaceTagsSettingsPage({route, policyTags}: WorkspaceTagsSettingsPag [route.params.policyID], ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} - > - {({policy}) => ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceTagsSettingsPage.displayName} - > - <HeaderWithBackButton title={translate('common.settings')} /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={policy?.errorFields?.requiresTag} - pendingAction={policy?.pendingFields?.requiresTag} - errorRowStyles={styles.mh5} - > - <View style={[styles.mt2, styles.mh4]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.requiresTag')}</Text> - <Switch - isOn={policy?.requiresTag ?? false} - accessibilityLabel={translate('workspace.tags.requiresTag')} - onToggle={updateWorkspaceRequiresTag} - /> - </View> + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + {({policy}) => ( + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceTagsSettingsPage.displayName} + > + <HeaderWithBackButton title={translate('common.settings')} /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={policy?.errorFields?.requiresTag} + pendingAction={policy?.pendingFields?.requiresTag} + errorRowStyles={styles.mh5} + > + <View style={[styles.mt2, styles.mh4]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.requiresTag')}</Text> + <Switch + isOn={policy?.requiresTag ?? false} + accessibilityLabel={translate('workspace.tags.requiresTag')} + onToggle={updateWorkspaceRequiresTag} + /> </View> - </OfflineWithFeedback> - <OfflineWithFeedback - errors={policyTags?.[policyTagName]?.errors} - pendingAction={policyTags?.[policyTagName]?.pendingAction} - errorRowStyles={styles.mh5} - > - <MenuItemWithTopDescription - title={policyTagName} - description={translate(`workspace.tags.customTagName`)} - onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))} - shouldShowRightIcon - /> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - )} - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + <OfflineWithFeedback + errors={policyTags?.[policyTagName]?.errors} + pendingAction={policyTags?.[policyTagName]?.pendingAction} + errorRowStyles={styles.mh5} + > + <MenuItemWithTopDescription + title={policyTagName} + description={translate(`workspace.tags.customTagName`)} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))} + shouldShowRightIcon + /> + </OfflineWithFeedback> + </View> + </ScreenWrapper> + )} + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/NamePage.tsx b/src/pages/workspace/taxes/NamePage.tsx index 5c77295a9664..ee52b7273c8f 100644 --- a/src/pages/workspace/taxes/NamePage.tsx +++ b/src/pages/workspace/taxes/NamePage.tsx @@ -16,9 +16,8 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -69,48 +68,46 @@ function NamePage({ } return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={NamePage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={NamePage.displayName} - > - <HeaderWithBackButton - title={translate('common.name')} - onBackButtonPress={goBack} - /> + <HeaderWithBackButton + title={translate('common.name')} + onBackButtonPress={goBack} + /> - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM} - submitButtonText={translate('workspace.editor.save')} - style={[styles.flexGrow1, styles.ph5]} - onSubmit={submit} - enabledWhenOffline - validate={validate} - > - <View style={styles.mb4}> - <InputWrapper - InputComponent={TextInput} - role={CONST.ROLE.PRESENTATION} - inputID={INPUT_IDS.NAME} - label={translate('workspace.editor.nameInputLabel')} - accessibilityLabel={translate('workspace.editor.nameInputLabel')} - value={name} - maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} - onChangeText={setName} - ref={inputCallbackRef} - /> - </View> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM} + submitButtonText={translate('workspace.editor.save')} + style={[styles.flexGrow1, styles.ph5]} + onSubmit={submit} + enabledWhenOffline + validate={validate} + > + <View style={styles.mb4}> + <InputWrapper + InputComponent={TextInput} + role={CONST.ROLE.PRESENTATION} + inputID={INPUT_IDS.NAME} + label={translate('workspace.editor.nameInputLabel')} + accessibilityLabel={translate('workspace.editor.nameInputLabel')} + value={name} + maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} + onChangeText={setName} + ref={inputCallbackRef} + /> + </View> + </FormProvider> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx index c062b6a13f62..90b9eda860c4 100644 --- a/src/pages/workspace/taxes/ValuePage.tsx +++ b/src/pages/workspace/taxes/ValuePage.tsx @@ -15,9 +15,8 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -55,52 +54,50 @@ function ValuePage({ } return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={ValuePage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={ValuePage.displayName} + <HeaderWithBackButton + title={translate('workspace.taxes.value')} + onBackButtonPress={goBack} + /> + + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM} + submitButtonText={translate('workspace.editor.save')} + style={[styles.flexGrow1]} + scrollContextEnabled + validate={validateTaxValue} + onSubmit={submit} + enabledWhenOffline + disablePressOnEnter={false} + shouldHideFixErrorsAlert + submitFlexEnabled={false} + submitButtonStyles={[styles.mh5, styles.mt0]} > - <HeaderWithBackButton - title={translate('workspace.taxes.value')} - onBackButtonPress={goBack} + <InputWrapper + InputComponent={AmountForm} + inputID={INPUT_IDS.VALUE} + defaultValue={defaultValue} + hideCurrencySymbol + // The default currency uses 2 decimal places, so we substract it + extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2} + // We increase the amount max length to support the extra decimals. + amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES} + extraSymbol={<Text style={styles.iouAmountText}>%</Text>} + ref={inputCallbackRef} /> - - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM} - submitButtonText={translate('workspace.editor.save')} - style={[styles.flexGrow1]} - scrollContextEnabled - validate={validateTaxValue} - onSubmit={submit} - enabledWhenOffline - disablePressOnEnter={false} - shouldHideFixErrorsAlert - submitFlexEnabled={false} - submitButtonStyles={[styles.mh5, styles.mt0]} - > - <InputWrapper - InputComponent={AmountForm} - inputID={INPUT_IDS.VALUE} - defaultValue={defaultValue} - hideCurrencySymbol - // The default currency uses 2 decimal places, so we substract it - extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2} - // We increase the amount max length to support the extra decimals. - amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES} - extraSymbol={<Text style={styles.iouAmountText}>%</Text>} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </FormProvider> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx index e76e57768963..3b75664fd23a 100644 --- a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx @@ -14,9 +14,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {createPolicyTax, getNextTaxCode, getTaxValueWithPercentage, validateTaxName, validateTaxValue} from '@libs/actions/TaxRate'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -63,62 +62,60 @@ function WorkspaceCreateTaxPage({ ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + testID={WorkspaceCreateTaxPage.displayName} + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} > - <ScreenWrapper - testID={WorkspaceCreateTaxPage.displayName} - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - > - <View style={[styles.h100, styles.flex1, styles.justifyContentBetween]}> - <HeaderWithBackButton title={translate('workspace.taxes.addRate')} /> - <FormProvider - style={[styles.flexGrow1, styles.mh5]} - formID={ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM} - onSubmit={submitForm} - validate={validateForm} - submitButtonText={translate('common.save')} - enabledWhenOffline - shouldValidateOnBlur={false} - disablePressOnEnter={false} - > - <View style={styles.mhn5}> - <InputWrapper - InputComponent={TextPicker} - inputID={INPUT_IDS.NAME} - label={translate('common.name')} - description={translate('common.name')} - rightLabel={translate('common.required')} - accessibilityLabel={translate('workspace.editor.nameInputLabel')} - maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} - multiline={false} - role={CONST.ROLE.PRESENTATION} - autoFocus - /> - <InputWrapper - InputComponent={AmountPicker} - inputID={INPUT_IDS.VALUE} - title={(v) => (v ? getTaxValueWithPercentage(v) : '')} - description={translate('workspace.taxes.value')} - rightLabel={translate('common.required')} - hideCurrencySymbol - // The default currency uses 2 decimal places, so we substract it - extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2} - // We increase the amount max length to support the extra decimals. - amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES} - extraSymbol={<Text style={styles.iouAmountText}>%</Text>} - /> - </View> - </FormProvider> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <View style={[styles.h100, styles.flex1, styles.justifyContentBetween]}> + <HeaderWithBackButton title={translate('workspace.taxes.addRate')} /> + <FormProvider + style={[styles.flexGrow1, styles.mh5]} + formID={ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM} + onSubmit={submitForm} + validate={validateForm} + submitButtonText={translate('common.save')} + enabledWhenOffline + shouldValidateOnBlur={false} + disablePressOnEnter={false} + > + <View style={styles.mhn5}> + <InputWrapper + InputComponent={TextPicker} + inputID={INPUT_IDS.NAME} + label={translate('common.name')} + description={translate('common.name')} + rightLabel={translate('common.required')} + accessibilityLabel={translate('workspace.editor.nameInputLabel')} + maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} + multiline={false} + role={CONST.ROLE.PRESENTATION} + autoFocus + /> + <InputWrapper + InputComponent={AmountPicker} + inputID={INPUT_IDS.VALUE} + title={(v) => (v ? getTaxValueWithPercentage(v) : '')} + description={translate('workspace.taxes.value')} + rightLabel={translate('common.required')} + hideCurrencySymbol + // The default currency uses 2 decimal places, so we substract it + extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2} + // We increase the amount max length to support the extra decimals. + amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES} + extraSymbol={<Text style={styles.iouAmountText}>%</Text>} + /> + </View> + </FormProvider> + </View> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index ec04b77df3ca..649ccf7579a9 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -19,9 +19,8 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -75,86 +74,84 @@ function WorkspaceEditTaxPage({ } return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + testID={WorkspaceEditTaxPage.displayName} + style={styles.mb5} > - <ScreenWrapper - testID={WorkspaceEditTaxPage.displayName} - style={styles.mb5} - > - <View style={[styles.h100, styles.flex1]}> - <HeaderWithBackButton + <View style={[styles.h100, styles.flex1]}> + <HeaderWithBackButton + title={currentTaxRate?.name} + threeDotsMenuItems={threeDotsMenuItems} + shouldShowThreeDotsButton={!!canEdit} + threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} + /> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'isDisabled')} + pendingAction={currentTaxRate?.pendingFields?.isDisabled} + errorRowStyles={styles.mh5} + onClose={() => clearTaxRateFieldError(policyID, taxID, 'isDisabled')} + > + <View style={[styles.mt2, styles.mh5]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text>{translate('workspace.taxes.actions.enable')}</Text> + <Switch + isOn={!currentTaxRate?.isDisabled} + accessibilityLabel={translate('workspace.taxes.actions.enable')} + onToggle={toggleTaxRate} + disabled={!canEdit} + /> + </View> + </View> + </OfflineWithFeedback> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'name')} + pendingAction={currentTaxRate?.pendingFields?.name} + errorRowStyles={styles.mh5} + onClose={() => clearTaxRateFieldError(policyID, taxID, 'name')} + > + <MenuItemWithTopDescription + shouldShowRightIcon title={currentTaxRate?.name} - threeDotsMenuItems={threeDotsMenuItems} - shouldShowThreeDotsButton={!!canEdit} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} + description={translate('common.name')} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAX_NAME.getRoute(`${policyID}`, taxID))} /> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'isDisabled')} - pendingAction={currentTaxRate?.pendingFields?.isDisabled} - errorRowStyles={styles.mh5} - onClose={() => clearTaxRateFieldError(policyID, taxID, 'isDisabled')} - > - <View style={[styles.mt2, styles.mh5]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text>{translate('workspace.taxes.actions.enable')}</Text> - <Switch - isOn={!currentTaxRate?.isDisabled} - accessibilityLabel={translate('workspace.taxes.actions.enable')} - onToggle={toggleTaxRate} - disabled={!canEdit} - /> - </View> - </View> - </OfflineWithFeedback> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'name')} - pendingAction={currentTaxRate?.pendingFields?.name} - errorRowStyles={styles.mh5} - onClose={() => clearTaxRateFieldError(policyID, taxID, 'name')} - > - <MenuItemWithTopDescription - shouldShowRightIcon - title={currentTaxRate?.name} - description={translate('common.name')} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAX_NAME.getRoute(`${policyID}`, taxID))} - /> - </OfflineWithFeedback> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'value')} - pendingAction={currentTaxRate?.pendingFields?.value} - errorRowStyles={styles.mh5} - onClose={() => clearTaxRateFieldError(policyID, taxID, 'value')} - > - <MenuItemWithTopDescription - shouldShowRightIcon - title={currentTaxRate?.value} - description={translate('workspace.taxes.value')} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAX_VALUE.getRoute(`${policyID}`, taxID))} - /> - </OfflineWithFeedback> - </View> - <ConfirmModal - title={translate('workspace.taxes.actions.delete')} - isVisible={isDeleteModalVisible} - onConfirm={deleteTaxRate} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={translate('workspace.taxes.deleteTaxConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </OfflineWithFeedback> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'value')} + pendingAction={currentTaxRate?.pendingFields?.value} + errorRowStyles={styles.mh5} + onClose={() => clearTaxRateFieldError(policyID, taxID, 'value')} + > + <MenuItemWithTopDescription + shouldShowRightIcon + title={currentTaxRate?.value} + description={translate('workspace.taxes.value')} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAX_VALUE.getRoute(`${policyID}`, taxID))} + /> + </OfflineWithFeedback> + </View> + <ConfirmModal + title={translate('workspace.taxes.actions.delete')} + isVisible={isDeleteModalVisible} + onConfirm={deleteTaxRate} + onCancel={() => setIsDeleteModalVisible(false)} + prompt={translate('workspace.taxes.deleteTaxConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index a61c8ae72734..afcb7ac08792 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -27,9 +27,8 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -238,69 +237,67 @@ function WorkspaceTaxesPage({ ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceTaxesPage.displayName} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceTaxesPage.displayName} - shouldShowOfflineIndicatorInWideScreen + <HeaderWithBackButton + icon={Illustrations.Coins} + title={translate('workspace.common.taxes')} + shouldShowBackButton={isSmallScreenWidth} > - <HeaderWithBackButton - icon={Illustrations.Coins} - title={translate('workspace.common.taxes')} - shouldShowBackButton={isSmallScreenWidth} - > - {!isSmallScreenWidth && headerButtons} - </HeaderWithBackButton> + {!isSmallScreenWidth && headerButtons} + </HeaderWithBackButton> - {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{headerButtons}</View>} + {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{headerButtons}</View>} - <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.taxes.subtitle')}</Text> - </View> - {isLoading && ( - <ActivityIndicator - size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} - style={[styles.flex1]} - color={theme.spinner} - /> - )} - <SelectionList - canSelectMultiple - sections={[{data: taxesList, isDisabled: false}]} - onCheckboxPress={toggleTax} - onSelectRow={navigateToEditTaxRate} - onSelectAll={toggleAllTaxes} - showScrollIndicator - ListItem={TableListItem} - customListHeader={getCustomListHeader()} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - onDismissError={(item) => (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)} + <View style={[styles.ph5, styles.pb5, styles.pt3]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.taxes.subtitle')}</Text> + </View> + {isLoading && ( + <ActivityIndicator + size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} + style={[styles.flex1]} + color={theme.spinner} /> - <ConfirmModal - title={translate('workspace.taxes.actions.delete')} - isVisible={isDeleteModalVisible} - onConfirm={deleteTaxes} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={ - selectedTaxesIDs.length > 1 - ? translate('workspace.taxes.deleteMultipleTaxConfirmation', {taxAmount: selectedTaxesIDs.length}) - : translate('workspace.taxes.deleteTaxConfirmation') - } - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + )} + <SelectionList + canSelectMultiple + sections={[{data: taxesList, isDisabled: false}]} + onCheckboxPress={toggleTax} + onSelectRow={navigateToEditTaxRate} + onSelectAll={toggleAllTaxes} + showScrollIndicator + ListItem={TableListItem} + customListHeader={getCustomListHeader()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + onDismissError={(item) => (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)} + /> + <ConfirmModal + title={translate('workspace.taxes.actions.delete')} + isVisible={isDeleteModalVisible} + onConfirm={deleteTaxes} + onCancel={() => setIsDeleteModalVisible(false)} + prompt={ + selectedTaxesIDs.length > 1 + ? translate('workspace.taxes.deleteMultipleTaxConfirmation', {taxAmount: selectedTaxesIDs.length}) + : translate('workspace.taxes.deleteTaxConfirmation') + } + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx index e9e359d9d059..073cd1b7adab 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx @@ -14,9 +14,8 @@ import {setPolicyCustomTaxName} from '@libs/actions/Policy'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as ValidationUtils from '@libs/ValidationUtils'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -55,47 +54,45 @@ function WorkspaceTaxesSettingsCustomTaxName({ }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceTaxesSettingsCustomTaxName.displayName} + style={styles.defaultModalContainer} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceTaxesSettingsCustomTaxName.displayName} - style={styles.defaultModalContainer} - > - <HeaderWithBackButton title={translate('workspace.taxes.customTaxName')} /> + <HeaderWithBackButton title={translate('workspace.taxes.customTaxName')} /> - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME} - submitButtonText={translate('workspace.editor.save')} - style={[styles.flexGrow1, styles.ph5]} - scrollContextEnabled - enabledWhenOffline - validate={validate} - onSubmit={submit} - > - <View style={styles.mb4}> - <InputWrapper - InputComponent={TextInput} - role={CONST.ROLE.PRESENTATION} - inputID={INPUT_IDS.NAME} - label={translate('workspace.editor.nameInputLabel')} - accessibilityLabel={translate('workspace.editor.nameInputLabel')} - defaultValue={policy?.taxRates?.name} - maxLength={CONST.TAX_RATES.CUSTOM_NAME_MAX_LENGTH} - multiline={false} - ref={inputCallbackRef} - /> - </View> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME} + submitButtonText={translate('workspace.editor.save')} + style={[styles.flexGrow1, styles.ph5]} + scrollContextEnabled + enabledWhenOffline + validate={validate} + onSubmit={submit} + > + <View style={styles.mb4}> + <InputWrapper + InputComponent={TextInput} + role={CONST.ROLE.PRESENTATION} + inputID={INPUT_IDS.NAME} + label={translate('workspace.editor.nameInputLabel')} + accessibilityLabel={translate('workspace.editor.nameInputLabel')} + defaultValue={policy?.taxRates?.name} + maxLength={CONST.TAX_RATES.CUSTOM_NAME_MAX_LENGTH} + multiline={false} + ref={inputCallbackRef} + /> + </View> + </FormProvider> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx index 3081df55fe69..d7aba1fa36a8 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx @@ -11,9 +11,8 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import type * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -44,36 +43,34 @@ function WorkspaceTaxesSettingsForeignCurrency({ }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceTaxesSettingsForeignCurrency.displayName} + style={styles.defaultModalContainer} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceTaxesSettingsForeignCurrency.displayName} - style={styles.defaultModalContainer} - > - {({insets}) => ( - <> - <HeaderWithBackButton title={translate('workspace.taxes.foreignDefault')} /> + {({insets}) => ( + <> + <HeaderWithBackButton title={translate('workspace.taxes.foreignDefault')} /> - <View style={[styles.mb4, styles.flex1]}> - <TaxPicker - selectedTaxRate={selectedTaxRate} - policyID={policyID} - insets={insets} - onSubmit={submit} - /> - </View> - </> - )} - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <View style={[styles.mb4, styles.flex1]}> + <TaxPicker + selectedTaxRate={selectedTaxRate} + policyID={policyID} + insets={insets} + onSubmit={submit} + /> + </View> + </> + )} + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx index 8fbfa7b79292..3ee015b6ad77 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx @@ -10,9 +10,8 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -55,40 +54,38 @@ function WorkspaceTaxesSettingsPage({ ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + testID={WorkspaceTaxesSettingsPage.displayName} + style={styles.defaultModalContainer} > - <ScreenWrapper - testID={WorkspaceTaxesSettingsPage.displayName} - style={styles.defaultModalContainer} - > - <ScrollView contentContainerStyle={styles.flexGrow1}> - <HeaderWithBackButton title={translate('common.settings')} /> - <View style={styles.flex1}> - {menuItems.map((item) => ( - <OfflineWithFeedback - key={item.description} - pendingAction={item.pendingAction} - > - <MenuItemWithTopDescription - shouldShowRightIcon - title={item.title} - description={item.description} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - onPress={item.action} - /> - </OfflineWithFeedback> - ))} - </View> - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <ScrollView contentContainerStyle={styles.flexGrow1}> + <HeaderWithBackButton title={translate('common.settings')} /> + <View style={styles.flex1}> + {menuItems.map((item) => ( + <OfflineWithFeedback + key={item.description} + pendingAction={item.pendingAction} + > + <MenuItemWithTopDescription + shouldShowRightIcon + title={item.title} + description={item.description} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + onPress={item.action} + /> + </OfflineWithFeedback> + ))} + </View> + </ScrollView> + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx index 630560f864b4..78f0b9bb5904 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx @@ -11,9 +11,8 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import type * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -40,36 +39,34 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({ }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper policyID={policyID}> + <FeatureEnabledAccessOrNotFoundWrapper + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceTaxesSettingsWorkspaceCurrency.displayName} + style={styles.defaultModalContainer} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceTaxesSettingsWorkspaceCurrency.displayName} - style={styles.defaultModalContainer} - > - {({insets}) => ( - <> - <HeaderWithBackButton title={translate('workspace.taxes.workspaceDefault')} /> + {({insets}) => ( + <> + <HeaderWithBackButton title={translate('workspace.taxes.workspaceDefault')} /> - <View style={[styles.mb4, styles.flex1]}> - <TaxPicker - selectedTaxRate={selectedTaxRate} - policyID={policyID} - insets={insets} - onSubmit={submit} - /> - </View> - </> - )} - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <View style={[styles.mb4, styles.flex1]}> + <TaxPicker + selectedTaxRate={selectedTaxRate} + policyID={policyID} + insets={insets} + onSubmit={submit} + /> + </View> + </> + )} + </ScreenWrapper> + </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx index da51b2c3e8e3..58f5d4c54e97 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -21,8 +21,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as UserUtils from '@libs/UserUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import * as Policy from '@userActions/Policy'; @@ -176,37 +175,35 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <FullPageNotFoundView - shouldShow={shouldShowNotFoundPage} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - onBackButtonPress={PolicyUtils.goBackFromInvalidPolicy} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <FullPageNotFoundView + shouldShow={shouldShowNotFoundPage} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + onBackButtonPress={PolicyUtils.goBackFromInvalidPolicy} + onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + testID={WorkspaceWorkflowsPayerPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - testID={WorkspaceWorkflowsPayerPage.displayName} - > - <HeaderWithBackButton - title={translate('workflowsPayerPage.title')} - subtitle={policyName} - onBackButtonPress={Navigation.goBack} - /> - <SelectionList - sections={sections} - textInputLabel={translate('optionsSelector.findMember')} - textInputValue={searchTerm} - onChangeText={setSearchTerm} - headerMessage={headerMessage} - ListItem={UserListItem} - onSelectRow={setPolicyAuthorizedPayer} - showScrollIndicator - /> - </ScreenWrapper> - </FullPageNotFoundView> - </PaidPolicyAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <HeaderWithBackButton + title={translate('workflowsPayerPage.title')} + subtitle={policyName} + onBackButtonPress={Navigation.goBack} + /> + <SelectionList + sections={sections} + textInputLabel={translate('optionsSelector.findMember')} + textInputValue={searchTerm} + onChangeText={setSearchTerm} + headerMessage={headerMessage} + ListItem={UserListItem} + onSelectRow={setPolicyAuthorizedPayer} + showScrollIndicator + /> + </ScreenWrapper> + </FullPageNotFoundView> + </AccessOrNotFoundWrapper> ); } From 5829f669b70f62d3d8787ac54e3bac121709ef6a Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Fri, 19 Apr 2024 12:15:07 +0200 Subject: [PATCH 231/580] cleanup --- .../AdminPolicyAccessOrNotFoundWrapper.tsx | 66 ------------------- .../PaidPolicyAccessOrNotFoundWrapper.tsx | 66 ------------------- 2 files changed, 132 deletions(-) delete mode 100644 src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx delete mode 100644 src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx diff --git a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx deleted file mode 100644 index 5c8456366c6b..000000000000 --- a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable rulesdir/no-negated-variables */ -import React, {useEffect} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import * as Policy from '@userActions/Policy'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -type AdminAccessOrNotFoundOnyxProps = { - /** The report currently being looked at */ - policy: OnyxEntry<OnyxTypes.Policy>; - - /** Indicated whether the report data is loading */ - isLoadingReportData: OnyxEntry<boolean>; -}; - -type AdminPolicyAccessOrNotFoundComponentProps = AdminAccessOrNotFoundOnyxProps & { - /** The children to render */ - children: ((props: AdminAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; - - /** The report currently being looked at */ - policyID: string; -}; - -function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFoundComponentProps) { - const isPolicyIDInRoute = !!props.policyID?.length; - - useEffect(() => { - if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { - // If the workspace is not required or is already loaded, we don't need to call the API - return; - } - - Policy.openWorkspace(props.policyID, []); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPolicyIDInRoute, props.policyID]); - - const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); - - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyAdmin(props.policy); - - if (shouldShowFullScreenLoadingIndicator) { - return <FullscreenLoadingIndicator />; - } - - if (shouldShowNotFoundPage) { - return <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; - } - - return typeof props.children === 'function' ? props.children(props) : props.children; -} - -export default withOnyx<AdminPolicyAccessOrNotFoundComponentProps, AdminAccessOrNotFoundOnyxProps>({ - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, - }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - }, -})(AdminPolicyAccessOrNotFoundComponent); diff --git a/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx deleted file mode 100644 index 9b6047493561..000000000000 --- a/src/pages/workspace/PaidPolicyAccessOrNotFoundWrapper.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* eslint-disable rulesdir/no-negated-variables */ -import React, {useEffect} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import * as Policy from '@userActions/Policy'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -type PaidPolicyAccessOrNotFoundOnyxProps = { - /** The report currently being looked at */ - policy: OnyxEntry<OnyxTypes.Policy>; - - /** Indicated whether the report data is loading */ - isLoadingReportData: OnyxEntry<boolean>; -}; - -type PaidPolicyAccessOrNotFoundComponentProps = PaidPolicyAccessOrNotFoundOnyxProps & { - /** The children to render */ - children: ((props: PaidPolicyAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; - - /** The report currently being looked at */ - policyID: string; -}; - -function PaidPolicyAccessOrNotFoundComponent(props: PaidPolicyAccessOrNotFoundComponentProps) { - const isPolicyIDInRoute = !!props.policyID?.length; - - useEffect(() => { - if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { - // If the workspace is not required or is already loaded, we don't need to call the API - return; - } - - Policy.openWorkspace(props.policyID, []); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPolicyIDInRoute, props.policyID]); - - const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); - - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPaidGroupPolicy(props.policy) || !props.policy.isPolicyExpenseChatEnabled; - - if (shouldShowFullScreenLoadingIndicator) { - return <FullscreenLoadingIndicator />; - } - - if (shouldShowNotFoundPage) { - return <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; - } - - return typeof props.children === 'function' ? props.children(props) : props.children; -} - -export default withOnyx<PaidPolicyAccessOrNotFoundComponentProps, PaidPolicyAccessOrNotFoundOnyxProps>({ - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, - }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - }, -})(PaidPolicyAccessOrNotFoundComponent); From f7e3f63315dbdae8a8bd05b8b0cda659d7c56f98 Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Fri, 19 Apr 2024 17:56:53 +0700 Subject: [PATCH 232/580] fix update icons in global create and money request flows --- .../getIconForAction/index.ts | 18 ++++++++++++++++++ .../AttachmentPickerWithMenuItems.tsx | 7 ++++--- .../FloatingActionButtonAndPopover.tsx | 11 ++++++++--- 3 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 src/libs/focusComposerWithDelay/getIconForAction/index.ts diff --git a/src/libs/focusComposerWithDelay/getIconForAction/index.ts b/src/libs/focusComposerWithDelay/getIconForAction/index.ts new file mode 100644 index 000000000000..a1b7ee001d5f --- /dev/null +++ b/src/libs/focusComposerWithDelay/getIconForAction/index.ts @@ -0,0 +1,18 @@ +import {ValueOf} from 'type-fest'; +import * as Expensicons from '@components/Icon/Expensicons'; +import CONST from '@src/CONST'; + +const getIconForAction = (actionType: ValueOf<typeof CONST.IOU.TYPE>) => { + switch (actionType) { + case CONST.IOU.TYPE.TRACK_EXPENSE: + return Expensicons.MoneyCircle; + case CONST.IOU.TYPE.REQUEST: + return Expensicons.Receipt; + case CONST.IOU.TYPE.SEND: + return Expensicons.Cash; + default: + return Expensicons.MoneyCircle; + } +}; + +export default getIconForAction; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 1294d2ca8aea..515ce8921c43 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -18,6 +18,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import getIconForAction from '@libs/focusComposerWithDelay/getIconForAction'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; @@ -129,17 +130,17 @@ function AttachmentPickerWithMenuItems({ onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? ''), }, [CONST.IOU.TYPE.REQUEST]: { - icon: Expensicons.MoneyCircle, + icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.REQUEST, report?.reportID ?? ''), }, [CONST.IOU.TYPE.SEND]: { - icon: Expensicons.Send, + icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID ?? ''), }, [CONST.IOU.TYPE.TRACK_EXPENSE]: { - icon: Expensicons.DocumentPlus, + icon: getIconForAction(CONST.IOU.TYPE.TRACK_EXPENSE), text: translate('iou.trackExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), }, diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 60fed8e7af2e..41acad611562 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -14,6 +14,7 @@ import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import getIconForAction from '@libs/focusComposerWithDelay/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import * as ReportUtils from '@libs/ReportUtils'; @@ -96,6 +97,10 @@ const getQuickActionIcon = (action: QuickActionName): React.FC<SvgProps> => { return Expensicons.Send; case CONST.QUICK_ACTIONS.ASSIGN_TASK: return Expensicons.Task; + case CONST.QUICK_ACTIONS.TRACK_DISTANCE: + case CONST.QUICK_ACTIONS.TRACK_MANUAL: + case CONST.QUICK_ACTIONS.TRACK_SCAN: + return getIconForAction(CONST.IOU.TYPE.TRACK_EXPENSE); default: return Expensicons.MoneyCircle; } @@ -288,7 +293,7 @@ function FloatingActionButtonAndPopover( ...(canUseTrackExpense ? [ { - icon: Expensicons.DocumentPlus, + icon: getIconForAction(CONST.IOU.TYPE.TRACK_EXPENSE), text: translate('iou.trackExpense'), onSelected: () => interceptAnonymousUser(() => @@ -304,7 +309,7 @@ function FloatingActionButtonAndPopover( ] : []), { - icon: Expensicons.MoneyCircle, + icon: getIconForAction(CONST.IOU.TYPE.REQUEST), text: translate('iou.submitExpense'), onSelected: () => interceptAnonymousUser(() => @@ -317,7 +322,7 @@ function FloatingActionButtonAndPopover( ), }, { - icon: Expensicons.Send, + icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {}), onSelected: () => interceptAnonymousUser(() => From 4a8ff63629644cbbaec2b172498a957e63327fc8 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:27:43 +0530 Subject: [PATCH 233/580] Update en.ts --- src/languages/en.ts | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 761399940d1a..0a8d2357f7a5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -336,8 +336,8 @@ export default { cameraPermissionRequired: 'Camera access', expensifyDoesntHaveAccessToCamera: "Expensify can't take photos without access to your camera. Tap Settings to update permissions.", attachmentError: 'Attachment error', - errorWhileSelectingAttachment: 'An error occurred while selecting an attachment, please try again', - errorWhileSelectingCorruptedImage: 'An error occurred while selecting a corrupted attachment, please try another file', + errorWhileSelectingAttachment: 'An error occurred while selecting an attachment, please try again.', + errorWhileSelectingCorruptedImage: 'An error occurred while selecting a corrupted attachment, please try another file.', takePhoto: 'Take photo', chooseFromGallery: 'Choose from gallery', chooseDocument: 'Choose document', @@ -582,7 +582,7 @@ export default { takePhoto: 'Take a photo', cameraAccess: 'Camera access is required to take pictures of receipts.', cameraErrorTitle: 'Camera Error', - cameraErrorMessage: 'An error occurred while taking a photo, please try again', + cameraErrorMessage: 'An error occurred while taking a photo, please try again.', dropTitle: 'Let it go', dropMessage: 'Drop your file here', flash: 'flash', @@ -1325,7 +1325,7 @@ export default { whatsYourName: "What's your name?", purpose: { title: 'What do you want to do today?', - error: 'Please make a selection before continuing', + error: 'Please make a selection before continuing.', [CONST.ONBOARDING_CHOICES.TRACK]: 'Track business spend for taxes', [CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Get paid back by my employer', [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: "Manage my team's expenses", @@ -1476,7 +1476,7 @@ export default { }, hasPhoneLoginError: 'To add a verified bank account please ensure your primary login is a valid email and try again. You can add your phone number as a secondary login.', hasBeenThrottledError: 'There was an error adding your bank account. Please wait a few minutes and try again.', - hasCurrencyError: 'Oops! It appears that your workspace currency is set to a different currency than USD. To proceed, please set it to USD and try again', + hasCurrencyError: 'Oops! It appears that your workspace currency is set to a different currency than USD. To proceed, please set it to USD and try again.', error: { youNeedToSelectAnOption: 'You need to select an option to proceed.', noBankAccountAvailable: 'Sorry, no bank account is available.', @@ -1560,7 +1560,7 @@ export default { legalMiddleNameLabel: 'Legal middle name', legalLastNameLabel: 'Legal last name', selectAnswer: 'You need to select a response to proceed.', - ssnFull9Error: 'Please enter a valid 9 digit SSN', + ssnFull9Error: 'Please enter a valid 9 digit SSN.', needSSNFull9: "We're having trouble verifying your SSN. Please enter the full 9 digits of your SSN.", weCouldNotVerify: 'We could not verify', pleaseFixIt: 'Please fix this information before continuing.', @@ -2070,8 +2070,8 @@ export default { unlockNoVBACopy: 'Connect a bank account to reimburse your workspace members online.', fastReimbursementsVBACopy: "You're all set to reimburse receipts from your bank account!", updateCustomUnitError: "Your changes couldn't be saved. The workspace was modified while you were offline, please try again.", - invalidRateError: 'Please enter a valid rate', - lowRateError: 'Rate must be greater than 0', + invalidRateError: 'Please enter a valid rate.', + lowRateError: 'Rate must be greater than 0.', }, accounting: { title: 'Connections', @@ -2155,7 +2155,7 @@ export default { inviteMessagePrompt: 'Make your invitation extra special by adding a message below', personalMessagePrompt: 'Message', genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', - inviteNoMembersError: 'Please select at least one member to invite', + inviteNoMembersError: 'Please select at least one member to invite.', }, distanceRates: { oopsNotSoFast: 'Oops! Not so fast...', @@ -2290,11 +2290,11 @@ export default { // eslint-disable-next-line @typescript-eslint/naming-convention public_announceDescription: 'Anyone can find this room', createRoom: 'Create room', - roomAlreadyExistsError: 'A room with this name already exists', + roomAlreadyExistsError: 'A room with this name already exists.', roomNameReservedError: ({reservedName}: RoomNameReservedErrorParams) => `${reservedName} is a default room on all workspaces. Please choose another name.`, - roomNameInvalidError: 'Room names can only include lowercase letters, numbers and hyphens', - pleaseEnterRoomName: 'Please enter a room name', - pleaseSelectWorkspace: 'Please select a workspace', + roomNameInvalidError: 'Room names can only include lowercase letters, numbers and hyphens.', + pleaseEnterRoomName: 'Please enter a room name.', + pleaseSelectWorkspace: 'Please select a workspace.', renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => ` renamed this room from ${oldName} to ${newName}`, roomRenamedTo: ({newName}: RoomRenamedToParams) => `Room renamed to ${newName}`, social: 'social', @@ -2382,7 +2382,7 @@ export default { }, generalError: { title: 'Attachment Error', - message: 'Attachment cannot be downloaded', + message: 'Attachment cannot be downloaded.', }, permissionError: { title: 'Storage access', @@ -2457,10 +2457,10 @@ export default { }, }, report: { - genericCreateReportFailureMessage: 'Unexpected error creating this chat, please try again later', - genericAddCommentFailureMessage: 'Unexpected error while posting the comment, please try again later', - genericUpdateReportFieldFailureMessage: 'Unexpected error while updating the field, please try again later', - genericUpdateReporNameEditFailureMessage: 'Unexpected error while renaming the report, please try again later', + genericCreateReportFailureMessage: 'Unexpected error creating this chat, please try again later.', + genericAddCommentFailureMessage: 'Unexpected error while posting the comment, please try again later.', + genericUpdateReportFieldFailureMessage: 'Unexpected error while updating the field, please try again later.', + genericUpdateReporNameEditFailureMessage: 'Unexpected error while renaming the report, please try again later.', noActivityYet: 'No activity yet', }, chronos: { From 36549a848d8a848d6646ff0210a0deab73f6ce94 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:36:33 +0530 Subject: [PATCH 234/580] Update es.ts --- src/languages/es.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 114577c9aea7..a00eed18b787 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -326,8 +326,8 @@ export default { cameraPermissionRequired: 'Permiso para acceder a la cámara', expensifyDoesntHaveAccessToCamera: 'Expensify no puede tomar fotos sin acceso a la cámara. Haz click en Configuración para actualizar los permisos.', attachmentError: 'Error al adjuntar archivo', - errorWhileSelectingAttachment: 'Ha ocurrido un error al seleccionar un archivo adjunto. Por favor, inténtalo de nuevo', - errorWhileSelectingCorruptedImage: 'Ha ocurrido un error al seleccionar un archivo adjunto corrupto. Por favor, inténtalo con otro archivo', + errorWhileSelectingAttachment: 'Ha ocurrido un error al seleccionar un archivo adjunto. Por favor, inténtalo de nuevo.', + errorWhileSelectingCorruptedImage: 'Ha ocurrido un error al seleccionar un archivo adjunto corrupto. Por favor, inténtalo con otro archivo.', takePhoto: 'Hacer una foto', chooseFromGallery: 'Elegir de la galería', chooseDocument: 'Elegir documento', @@ -1584,7 +1584,7 @@ export default { legalMiddleNameLabel: 'Segundo nombre legal', legalLastNameLabel: 'Apellidos legales', selectAnswer: 'Selecciona una respuesta.', - ssnFull9Error: 'Por favor, introduce los 9 dígitos de un número de seguridad social válido', + ssnFull9Error: 'Por favor, introduce los 9 dígitos de un número de seguridad social válido.', needSSNFull9: 'Estamos teniendo problemas para verificar tu número de seguridad social. Introduce los 9 dígitos del número de seguridad social.', weCouldNotVerify: 'No se pudo verificar', pleaseFixIt: 'Corrige esta información antes de continuar.', @@ -2133,8 +2133,8 @@ export default { unlockNoVBACopy: 'Conecta una cuenta bancaria para reembolsar online a los miembros de tu espacio de trabajo.', fastReimbursementsVBACopy: '¡Todo listo para reembolsar recibos desde tu cuenta bancaria!', updateCustomUnitError: 'Los cambios no han podido ser guardados. El espacio de trabajo ha sido modificado mientras estabas desconectado. Por favor, inténtalo de nuevo.', - invalidRateError: 'Por favor, introduce una tarifa válida', - lowRateError: 'La tarifa debe ser mayor que 0', + invalidRateError: 'Por favor, introduce una tarifa válida.', + lowRateError: 'La tarifa debe ser mayor que 0.', }, bills: { manageYourBills: 'Gestiona tus facturas', @@ -2182,7 +2182,7 @@ export default { inviteMessageTitle: 'Añadir un mensaje', inviteMessagePrompt: 'Añadir un mensaje para hacer tu invitación destacar', personalMessagePrompt: 'Mensaje', - inviteNoMembersError: 'Por favor, selecciona al menos un miembro a invitar', + inviteNoMembersError: 'Por favor, selecciona al menos un miembro a invitar.', genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', }, distanceRates: { @@ -2320,12 +2320,12 @@ export default { // eslint-disable-next-line @typescript-eslint/naming-convention public_announceDescription: 'Cualquier persona puede unirse a esta sala', createRoom: 'Crea una sala de chat', - roomAlreadyExistsError: 'Ya existe una sala con este nombre', + roomAlreadyExistsError: 'Ya existe una sala con este nombre.', roomNameReservedError: ({reservedName}: RoomNameReservedErrorParams) => `${reservedName} es el nombre una sala por defecto de todos los espacios de trabajo. Por favor, elige otro nombre.`, - roomNameInvalidError: 'Los nombres de las salas solo pueden contener minúsculas, números y guiones', - pleaseEnterRoomName: 'Por favor, escribe el nombre de una sala', - pleaseSelectWorkspace: 'Por favor, selecciona un espacio de trabajo', + roomNameInvalidError: 'Los nombres de las salas solo pueden contener minúsculas, números y guiones.', + pleaseEnterRoomName: 'Por favor, escribe el nombre de una sala.', + pleaseSelectWorkspace: 'Por favor, selecciona un espacio de trabajo.', renamedRoomAction: ({oldName, newName}: RenamedRoomActionParams) => ` cambió el nombre de la sala de ${oldName} a ${newName}`, roomRenamedTo: ({newName}: RoomRenamedToParams) => `Sala renombrada a ${newName}`, social: 'social', @@ -2414,7 +2414,7 @@ export default { }, generalError: { title: 'Error en la descarga', - message: 'No se puede descargar el archivo adjunto', + message: 'No se puede descargar el archivo adjunto.', }, permissionError: { title: 'Permiso para acceder al almacenamiento', @@ -2489,9 +2489,9 @@ export default { }, }, report: { - genericCreateReportFailureMessage: 'Error inesperado al crear el chat. Por favor, inténtalo más tarde', - genericAddCommentFailureMessage: 'Error inesperado al añadir el comentario. Por favor, inténtalo más tarde', - genericUpdateReportFieldFailureMessage: 'Error inesperado al actualizar el campo. Por favor, inténtalo más tarde', + genericCreateReportFailureMessage: 'Error inesperado al crear el chat. Por favor, inténtalo más tarde.', + genericAddCommentFailureMessage: 'Error inesperado al añadir el comentario. Por favor, inténtalo más tarde.', + genericUpdateReportFieldFailureMessage: 'Error inesperado al actualizar el campo. Por favor, inténtalo más tarde.', genericUpdateReporNameEditFailureMessage: 'Error inesperado al cambiar el nombre del informe. Vuelva a intentarlo más tarde.', noActivityYet: 'Sin actividad todavía', }, From 7503447ca1428a55e701d961565bec5d0be1984c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= <contact@fabiohenriques.dev> Date: Fri, 19 Apr 2024 12:06:57 +0100 Subject: [PATCH 235/580] Remove almost all JS leftovers --- src/CONST.ts | 71 ++++--- src/components/Modal/modalPropTypes.js | 89 --------- src/components/PDFView/pdfViewPropTypes.js | 51 ----- src/components/Popover/popoverPropTypes.js | 48 ----- .../Reactions/EmojiReactionsPropTypes.js | 31 --- .../BaseTextInput/baseTextInputPropTypes.js | 139 -------------- .../textInputWithCurrencySymbolPropTypes.js | 49 ----- src/components/bankAccountPropTypes.js | 15 -- src/components/cardPropTypes.js | 12 -- src/components/menuItemPropTypes.js | 179 ------------------ src/components/networkPropTypes.js | 12 -- src/components/refPropTypes.js | 3 - src/components/taxPropTypes.js | 29 --- src/components/transactionsDraftPropTypes.js | 37 ---- .../withCurrentUserPersonalDetails.tsx | 11 -- src/libs/actions/BankAccounts.ts | 3 +- src/libs/actions/Plaid.ts | 4 +- .../resetFreePlanBankAccount.ts | 6 +- .../EnablePayments/userWalletPropTypes.js | 31 --- .../BankInfo/substeps/Confirmation.tsx | 2 +- .../ConfirmationUBO.tsx | 2 +- .../substeps/ConfirmationBusiness.tsx | 2 +- .../PersonalInfo/substeps/Confirmation.tsx | 2 +- .../ReimbursementAccountPage.tsx | 5 +- .../plaidDataPropTypes.js | 52 ----- .../reimbursementAccountPropTypes.js | 47 ----- .../validateLinkPropTypes.js | 24 --- .../home/report/reportActionSourcePropType.js | 3 - .../step/IOURequestStepRoutePropTypes.js | 29 --- src/pages/iouReportPropTypes.js | 18 -- src/pages/nextStepPropTypes.js | 48 ----- src/pages/personalDetailsPropType.js | 53 ------ src/pages/policyMemberPropType.js | 15 -- src/pages/reportMetadataPropTypes.js | 12 -- src/pages/safeAreaInsetPropTypes.js | 10 - .../settings/Wallet/assignedCardPropTypes.js | 18 -- .../workspace/WorkspacePageWithSections.tsx | 4 +- src/pages/workspace/withPolicy.tsx | 31 ++- src/types/onyx/PlaidData.ts | 1 + 39 files changed, 89 insertions(+), 1109 deletions(-) delete mode 100644 src/components/Modal/modalPropTypes.js delete mode 100644 src/components/PDFView/pdfViewPropTypes.js delete mode 100644 src/components/Popover/popoverPropTypes.js delete mode 100644 src/components/Reactions/EmojiReactionsPropTypes.js delete mode 100644 src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js delete mode 100644 src/components/TextInputWithCurrencySymbol/textInputWithCurrencySymbolPropTypes.js delete mode 100644 src/components/bankAccountPropTypes.js delete mode 100644 src/components/cardPropTypes.js delete mode 100644 src/components/menuItemPropTypes.js delete mode 100644 src/components/networkPropTypes.js delete mode 100644 src/components/refPropTypes.js delete mode 100644 src/components/taxPropTypes.js delete mode 100644 src/components/transactionsDraftPropTypes.js delete mode 100644 src/pages/EnablePayments/userWalletPropTypes.js delete mode 100644 src/pages/ReimbursementAccount/plaidDataPropTypes.js delete mode 100644 src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js delete mode 100644 src/pages/ValidateLoginPage/validateLinkPropTypes.js delete mode 100644 src/pages/home/report/reportActionSourcePropType.js delete mode 100644 src/pages/iou/request/step/IOURequestStepRoutePropTypes.js delete mode 100644 src/pages/iouReportPropTypes.js delete mode 100644 src/pages/nextStepPropTypes.js delete mode 100644 src/pages/personalDetailsPropType.js delete mode 100644 src/pages/policyMemberPropType.js delete mode 100644 src/pages/reportMetadataPropTypes.js delete mode 100644 src/pages/safeAreaInsetPropTypes.js delete mode 100644 src/pages/settings/Wallet/assignedCardPropTypes.js diff --git a/src/CONST.ts b/src/CONST.ts index a840cb481a1a..078612dbe0cb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4,6 +4,7 @@ import dateSubtract from 'date-fns/sub'; import Config from 'react-native-config'; import * as KeyCommand from 'react-native-key-command'; import type {ValueOf} from 'type-fest'; +import BankAccount from './libs/models/BankAccount'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; @@ -1343,6 +1344,14 @@ const CONST = { ERROR: 'ERROR', EXIT: 'EXIT', }, + DEFAULT_DATA: { + bankName: '', + plaidAccessToken: '', + bankAccounts: [] as [], + isLoading: false, + error: '', + errors: {}, + }, }, ONFIDO: { @@ -3625,31 +3634,43 @@ const CONST = { DEBUG: 'DEBUG', }, }, - REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX: { - BANK_ACCOUNT: { - ACCOUNT_NUMBERS: 0, - }, - PERSONAL_INFO: { - LEGAL_NAME: 0, - DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, - }, - BUSINESS_INFO: { - BUSINESS_NAME: 0, - TAX_ID_NUMBER: 1, - COMPANY_WEBSITE: 2, - PHONE_NUMBER: 3, - COMPANY_ADDRESS: 4, - COMPANY_TYPE: 5, - INCORPORATION_DATE: 6, - INCORPORATION_STATE: 7, - }, - UBO: { - LEGAL_NAME: 0, - DATE_OF_BIRTH: 1, - SSN: 2, - ADDRESS: 3, + REIMBURSEMENT_ACCOUNT: { + DEFAULT_DATA: { + achData: { + state: BankAccount.STATE.SETUP, + }, + isLoading: false, + errorFields: {}, + errors: {}, + maxAttemptsReached: false, + shouldShowResetModal: false, + }, + SUBSTEP_INDEX: { + BANK_ACCOUNT: { + ACCOUNT_NUMBERS: 0, + }, + PERSONAL_INFO: { + LEGAL_NAME: 0, + DATE_OF_BIRTH: 1, + SSN: 2, + ADDRESS: 3, + }, + BUSINESS_INFO: { + BUSINESS_NAME: 0, + TAX_ID_NUMBER: 1, + COMPANY_WEBSITE: 2, + PHONE_NUMBER: 3, + COMPANY_ADDRESS: 4, + COMPANY_TYPE: 5, + INCORPORATION_DATE: 6, + INCORPORATION_STATE: 7, + }, + UBO: { + LEGAL_NAME: 0, + DATE_OF_BIRTH: 1, + SSN: 2, + ADDRESS: 3, + }, }, }, CURRENCY_TO_DEFAULT_MILEAGE_RATE: JSON.parse(`{ diff --git a/src/components/Modal/modalPropTypes.js b/src/components/Modal/modalPropTypes.js deleted file mode 100644 index 84e610b694e4..000000000000 --- a/src/components/Modal/modalPropTypes.js +++ /dev/null @@ -1,89 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import stylePropTypes from '@styles/stylePropTypes'; -import CONST from '@src/CONST'; - -const propTypes = { - /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ - fullscreen: PropTypes.bool, - - /** Should we close modal on outside click */ - shouldCloseOnOutsideClick: PropTypes.bool, - - /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility: PropTypes.bool, - - /** Callback method fired when the user requests to close the modal */ - onClose: PropTypes.func.isRequired, - - /** State that determines whether to display the modal or not */ - isVisible: PropTypes.bool.isRequired, - - /** Modal contents */ - children: PropTypes.node.isRequired, - - /** Callback method fired when the user requests to submit the modal content. */ - onSubmit: PropTypes.func, - - /** Callback method fired when the modal is hidden */ - onModalHide: PropTypes.func, - - /** Callback method fired when the modal is shown */ - onModalShow: PropTypes.func, - - /** Style of modal to display */ - type: PropTypes.oneOf(_.values(CONST.MODAL.MODAL_TYPE)), - - /** A react-native-animatable animation definition for the modal display animation. */ - animationIn: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - - /** A react-native-animatable animation definition for the modal hide animation. */ - animationOut: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - - /** The anchor position of a popover modal. Has no effect on other modal types. */ - popoverAnchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - - /** Modal container styles */ - innerContainerStyle: stylePropTypes, - - /** Whether the modal should go under the system statusbar */ - statusBarTranslucent: PropTypes.bool, - - /** Whether the modal should avoid the keyboard */ - avoidKeyboard: PropTypes.bool, - - /** - * Whether the modal should hide its content while animating. On iOS, set to true - * if `useNativeDriver` is also true, to avoid flashes in the UI. - * - * See: https://github.com/react-native-modal/react-native-modal/pull/116 - * */ - hideModalContentWhileAnimating: PropTypes.bool, - - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - fullscreen: true, - shouldCloseOnOutsideClick: false, - shouldSetModalVisibility: true, - onSubmit: null, - type: '', - onModalHide: () => {}, - onModalShow: () => {}, - animationIn: null, - animationOut: null, - popoverAnchorPosition: {}, - innerContainerStyle: {}, - statusBarTranslucent: true, - avoidKeyboard: false, - hideModalContentWhileAnimating: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/PDFView/pdfViewPropTypes.js b/src/components/PDFView/pdfViewPropTypes.js deleted file mode 100644 index 546f9b1a8933..000000000000 --- a/src/components/PDFView/pdfViewPropTypes.js +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; -import {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import stylePropTypes from '@styles/stylePropTypes'; - -const propTypes = { - /** URL to full-sized image */ - sourceURL: PropTypes.string, - - /** PDF file name */ - fileName: PropTypes.string, - - /** Additional style props */ - style: stylePropTypes, - - /** Notify parent that the keyboard has opened or closed */ - onToggleKeyboard: PropTypes.func, - - /** Handles press events like toggling attachment arrows natively */ - onPress: PropTypes.func, - - /** Handles scale changed event in PDF component */ - onScaleChanged: PropTypes.func, - - /** Handles load complete event in PDF component */ - onLoadComplete: PropTypes.func, - - /** Should focus to the password input */ - isFocused: PropTypes.bool, - - /** Styles for the error label */ - errorLabelStyles: stylePropTypes, - - ...windowDimensionsPropTypes, - - ...withThemeStylesPropTypes, -}; - -const defaultProps = { - sourceURL: '', - fileName: '', - style: {}, - onPress: undefined, - onToggleKeyboard: () => {}, - onScaleChanged: () => {}, - onLoadComplete: () => {}, - isFocused: false, - errorLabelStyles: [], -}; - -export {propTypes, defaultProps}; diff --git a/src/components/Popover/popoverPropTypes.js b/src/components/Popover/popoverPropTypes.js deleted file mode 100644 index c758c4e6d311..000000000000 --- a/src/components/Popover/popoverPropTypes.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import {defaultProps as defaultModalProps, propTypes as modalPropTypes} from '@components/Modal/modalPropTypes'; -import refPropTypes from '@components/refPropTypes'; -import CONST from '@src/CONST'; - -const propTypes = { - ..._.omit(modalPropTypes, ['type', 'popoverAnchorPosition']), - - /** The anchor position of the popover */ - anchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - - /** The anchor ref of the popover */ - anchorRef: refPropTypes, - - /** A react-native-animatable animation timing for the modal display animation. */ - animationInTiming: PropTypes.number, - - /** Whether disable the animations */ - disableAnimation: PropTypes.bool, - - /** The ref of the popover */ - withoutOverlayRef: refPropTypes, - - /** Whether we want to show the popover on the right side of the screen */ - fromSidebarMediumScreen: PropTypes.bool, -}; - -const defaultProps = { - ..._.omit(defaultModalProps, ['type', 'popoverAnchorPosition']), - - animationIn: 'fadeIn', - animationOut: 'fadeOut', - animationInTiming: CONST.ANIMATED_TRANSITION, - - // Anchor position is optional only because it is not relevant on mobile - anchorPosition: {}, - anchorRef: () => {}, - disableAnimation: true, - withoutOverlayRef: () => {}, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/Reactions/EmojiReactionsPropTypes.js b/src/components/Reactions/EmojiReactionsPropTypes.js deleted file mode 100644 index ace88abefcd9..000000000000 --- a/src/components/Reactions/EmojiReactionsPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -/** All the emoji reactions for the report action. An object that looks like this: - "emojiReactions": { - "+1": { // The emoji added to the action - "createdAt": "2021-01-01 00:00:00", - "users": { - 2352342: { // The accountID of the user who added this emoji - "skinTones": { - "1": "2021-01-01 00:00:00", - "2": "2021-01-01 00:00:00", - }, - }, - }, - }, - }, -*/ -export default PropTypes.objectOf( - PropTypes.shape({ - /** The time the emoji was added */ - createdAt: PropTypes.string, - - /** All the users who have added this emoji */ - users: PropTypes.objectOf( - PropTypes.shape({ - /** The skin tone which was used and also the timestamp of when it was added */ - skinTones: PropTypes.objectOf(PropTypes.string), - }), - ), - }), -); diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js deleted file mode 100644 index b0a73a650d82..000000000000 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ /dev/null @@ -1,139 +0,0 @@ -import PropTypes from 'prop-types'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; -import {translatableTextPropTypes} from '@libs/Localize'; - -const propTypes = { - /** Input label */ - label: PropTypes.string, - - /** Name attribute for the input */ - name: PropTypes.string, - - /** Input value */ - value: PropTypes.string, - - /** Default value - used for non controlled inputs */ - defaultValue: PropTypes.string, - - /** Input value placeholder */ - placeholder: PropTypes.string, - - /** Error text to display */ - errorText: translatableTextPropTypes, - - /** Icon to display in right side of text input */ - icon: sourcePropTypes, - - /** Customize the TextInput container */ - textInputContainerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customizes the touchable wrapper of the TextInput component */ - touchableInputWrapperStyle: PropTypes.arrayOf(PropTypes.object), - - /** Customize the main container */ - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** input style */ - inputStyle: PropTypes.arrayOf(PropTypes.object), - - /** If present, this prop forces the label to remain in a position where it will not collide with input text */ - forceActiveLabel: PropTypes.bool, - - /** Should the input auto focus? */ - autoFocus: PropTypes.bool, - - /** Disable the virtual keyboard */ - disableKeyboard: PropTypes.bool, - - /** - * Autogrow input container length based on the entered text. - * Note: If you use this prop, the text input has to be controlled - * by a value prop. - */ - autoGrow: PropTypes.bool, - - /** - * Autogrow input container height based on the entered text - * Note: If you use this prop, the text input has to be controlled - * by a value prop. - */ - autoGrowHeight: PropTypes.bool, - - /** Hide the focus styles on TextInput */ - hideFocusedState: PropTypes.bool, - - /** Forward the inner ref */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - - /** Maximum characters allowed */ - maxLength: PropTypes.number, - - /** Hint text to display below the TextInput */ - hint: translatableTextPropTypes, - - /** Prefix character */ - prefixCharacter: PropTypes.string, - - /** Whether autoCorrect functionality should enable */ - autoCorrect: PropTypes.bool, - - /** Form props */ - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback to update the value on Form when input is used in the Form component. */ - onInputChange: PropTypes.func, - - /** Whether we should wait before focusing the TextInput, useful when using transitions */ - shouldDelayFocus: PropTypes.bool, - - /** Indicate whether input is multiline */ - multiline: PropTypes.bool, - - /** Set the default value to the input if there is a valid saved value */ - shouldUseDefaultValue: PropTypes.bool, - - /** Indicate whether or not the input should prevent swipe actions in tabs */ - shouldInterceptSwipe: PropTypes.bool, -}; - -const defaultProps = { - label: '', - name: '', - errorText: '', - placeholder: '', - hasError: false, - containerStyles: [], - textInputContainerStyles: [], - inputStyle: [], - autoFocus: false, - autoCorrect: true, - - /** - * To be able to function as either controlled or uncontrolled component we should not - * assign a default prop value for `value` or `defaultValue` props - */ - value: undefined, - defaultValue: undefined, - forceActiveLabel: false, - disableKeyboard: false, - autoGrow: false, - autoGrowHeight: false, - hideFocusedState: false, - innerRef: () => {}, - shouldSaveDraft: false, - maxLength: null, - hint: '', - prefixCharacter: '', - onInputChange: () => {}, - shouldDelayFocus: false, - icon: null, - shouldUseDefaultValue: false, - multiline: false, - shouldInterceptSwipe: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/TextInputWithCurrencySymbol/textInputWithCurrencySymbolPropTypes.js b/src/components/TextInputWithCurrencySymbol/textInputWithCurrencySymbolPropTypes.js deleted file mode 100644 index bf292b331b81..000000000000 --- a/src/components/TextInputWithCurrencySymbol/textInputWithCurrencySymbolPropTypes.js +++ /dev/null @@ -1,49 +0,0 @@ -import PropTypes from 'prop-types'; -import refPropTypes from '@components/refPropTypes'; - -const propTypes = { - /** A ref to forward to amount text input */ - forwardedRef: refPropTypes, - - /** Formatted amount in local currency */ - formattedAmount: PropTypes.string.isRequired, - - /** Function to call when amount in text input is changed */ - onChangeAmount: PropTypes.func, - - /** Function to call when currency button is pressed */ - onCurrencyButtonPress: PropTypes.func, - - /** Placeholder value for amount text input */ - placeholder: PropTypes.string.isRequired, - - /** Currency code of user's selected currency */ - selectedCurrencyCode: PropTypes.string.isRequired, - - /** Selection Object */ - selection: PropTypes.shape({ - start: PropTypes.number, - end: PropTypes.number, - }), - - /** Function to call when selection in text input is changed */ - onSelectionChange: PropTypes.func, - - /** Function to call to handle key presses in the text input */ - onKeyPress: PropTypes.func, - - /** Whether the currency symbol is pressable */ - isCurrencyPressable: PropTypes.bool, -}; - -const defaultProps = { - forwardedRef: undefined, - onChangeAmount: () => {}, - onCurrencyButtonPress: () => {}, - selection: undefined, - onSelectionChange: () => {}, - onKeyPress: () => {}, - isCurrencyPressable: true, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/bankAccountPropTypes.js b/src/components/bankAccountPropTypes.js deleted file mode 100644 index 3331a617cbce..000000000000 --- a/src/components/bankAccountPropTypes.js +++ /dev/null @@ -1,15 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** The name of the institution (bank of america, etc */ - addressName: PropTypes.string, - - /** The masked bank account number */ - accountNumber: PropTypes.string, - - /** The bankAccountID in the bankAccounts db */ - bankAccountID: PropTypes.number, - - /** The bank account type */ - type: PropTypes.string, -}); diff --git a/src/components/cardPropTypes.js b/src/components/cardPropTypes.js deleted file mode 100644 index 306f10a992c1..000000000000 --- a/src/components/cardPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** The name of the institution (bank of america, etc) */ - cardName: PropTypes.string, - - /** The masked credit card number */ - cardNumber: PropTypes.string, - - /** The ID of the card in the cards DB */ - cardID: PropTypes.number, -}); diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js deleted file mode 100644 index 80ae1edd5176..000000000000 --- a/src/components/menuItemPropTypes.js +++ /dev/null @@ -1,179 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import stylePropTypes from '@styles/stylePropTypes'; -import CONST from '@src/CONST'; -import avatarPropTypes from './avatarPropTypes'; -import sourcePropTypes from './Image/sourcePropTypes'; -import refPropTypes from './refPropTypes'; - -const propTypes = { - /** Text to be shown as badge near the right end. */ - badgeText: PropTypes.string, - - /** Any additional styles to apply */ - // eslint-disable-next-line react/forbid-prop-types - wrapperStyle: stylePropTypes, - - /** Used to apply offline styles to child text components */ - style: stylePropTypes, - - /** Used to apply styles specifically to the title */ - titleStyle: stylePropTypes, - - /** Function to fire when component is pressed */ - onPress: PropTypes.func, - - /** Icon to display on the left side of component */ - icon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes, PropTypes.arrayOf(avatarPropTypes)]), - - /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon: sourcePropTypes, - - /** Icon Width */ - iconWidth: PropTypes.number, - - /** Icon Height */ - iconHeight: PropTypes.number, - - /** Text to display for the item */ - title: PropTypes.string, - - /** Text that appears above the title */ - label: PropTypes.string, - - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon: PropTypes.bool, - - /** Icon to display at right side of title */ - titleIcon: sourcePropTypes, - - /** Boolean whether to display the right icon */ - shouldShowRightIcon: PropTypes.bool, - - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState: PropTypes.bool, - - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle: PropTypes.bool, - - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop: PropTypes.bool, - - /** Whether this item is selected */ - isSelected: PropTypes.bool, - - /** A boolean flag that gives the icon a green fill if true */ - success: PropTypes.bool, - - /** Overrides the icon for shouldShowRightIcon */ - iconRight: sourcePropTypes, - - /** A description text to show under the title */ - description: PropTypes.string, - - /** Any additional styles to pass to the icon container. */ - iconStyles: PropTypes.arrayOf(PropTypes.object), - - /** The fill color to pass into the icon. */ - iconFill: PropTypes.string, - - /** The fill color to pass into the secondary icon. */ - secondaryIconFill: PropTypes.string, - - /** Whether item is focused or active */ - focused: PropTypes.bool, - - /** Should we disable this menu item? */ - disabled: PropTypes.bool, - - /** A right-aligned subtitle for this menu option */ - subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - - /** Flag to choose between avatar image or an icon */ - iconType: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_ICON, CONST.ICON_TYPE_WORKSPACE]), - - /** Whether the menu item should be interactive at all */ - interactive: PropTypes.bool, - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), - - /** Avatars to show on the right of the menu item */ - floatRightAvatars: PropTypes.arrayOf(avatarPropTypes), - - /** The type of brick road indicator to show. */ - brickRoadIndicator: PropTypes.oneOf([CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, CONST.BRICK_ROAD_INDICATOR_STATUS.INFO, '']), - - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: PropTypes.bool, - - /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), - - /** Prop to represent the size of the avatar images to be shown */ - avatarSize: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), - - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: PropTypes.func, - - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection: PropTypes.bool, - - /** The ref to the menu item */ - forwardedRef: refPropTypes, - - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: PropTypes.arrayOf(PropTypes.object), - - /** Text to display under the main item */ - furtherDetails: PropTypes.string, - - /** An icon to display under the main item */ - furtherDetailsIcon: PropTypes.oneOfType([PropTypes.elementType, PropTypes.string]), - - /** The action accept for anonymous user or not */ - isAnonymousAction: PropTypes.bool, - - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu: PropTypes.bool, - - /** The max number of lines the title text should occupy before ellipses are added */ - numberOfLines: PropTypes.number, - - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled: PropTypes.bool, - - /** Error to display below the title */ - error: PropTypes.string, - - /** Should render the content in HTML format */ - shouldRenderAsHTML: PropTypes.bool, - - /** Label to be displayed on the right */ - rightLabel: PropTypes.string, - - /** Component to be displayed on the right */ - rightComponent: PropTypes.node, - - /** Should render component on the right */ - shouldShowRightComponent: PropTypes.bool, - - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: PropTypes.arrayOf(PropTypes.object), - - /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress: PropTypes.bool, - - shouldPutLeftPaddingWhenNoIcon: PropTypes.bool, - - /** The menu item link or function to get the link */ - link: PropTypes.oneOfType(PropTypes.func, PropTypes.string), - - /** Icon should be displayed in its own color */ - displayInDefaultIconColor: PropTypes.bool, - - /** Is this menu item in the settings pane */ - isPaneMenu: PropTypes.bool, -}; - -export default propTypes; diff --git a/src/components/networkPropTypes.js b/src/components/networkPropTypes.js deleted file mode 100644 index 40cbf990d81f..000000000000 --- a/src/components/networkPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** Is the network currently offline or not */ - isOffline: PropTypes.bool, - - /** Should the network be forced offline */ - shouldForceOffline: PropTypes.bool, - - /** Whether we should fail all network requests */ - shouldFailAllRequests: PropTypes.bool, -}); diff --git a/src/components/refPropTypes.js b/src/components/refPropTypes.js deleted file mode 100644 index a67f6323d1d9..000000000000 --- a/src/components/refPropTypes.js +++ /dev/null @@ -1,3 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOfType([PropTypes.func, PropTypes.object]); diff --git a/src/components/taxPropTypes.js b/src/components/taxPropTypes.js deleted file mode 100644 index 98c3a4a75257..000000000000 --- a/src/components/taxPropTypes.js +++ /dev/null @@ -1,29 +0,0 @@ -import PropTypes from 'prop-types'; - -const taxPropTypes = PropTypes.shape({ - /** Name of a tax */ - name: PropTypes.string, - - /** The value of a tax */ - value: PropTypes.string, - - /** Whether the tax is disabled */ - isDisabled: PropTypes.bool, -}); - -export default PropTypes.shape({ - /** Name of the tax */ - name: PropTypes.string, - - /** Default policy tax ID */ - defaultExternalID: PropTypes.string, - - /** Default value of taxes */ - defaultValue: PropTypes.string, - - /** Default foreign policy tax ID */ - foreignTaxDefault: PropTypes.string, - - /** List of tax names and values */ - taxes: PropTypes.objectOf(taxPropTypes), -}); diff --git a/src/components/transactionsDraftPropTypes.js b/src/components/transactionsDraftPropTypes.js deleted file mode 100644 index ca14c4537aa7..000000000000 --- a/src/components/transactionsDraftPropTypes.js +++ /dev/null @@ -1,37 +0,0 @@ -import PropTypes from 'prop-types'; - -const dataPropTypes = PropTypes.shape({ - name: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - code: PropTypes.string, -}); - -const taxRatePropTypes = PropTypes.shape({ - text: PropTypes.string.isRequired, - keyForList: PropTypes.string.isRequired, - searchText: PropTypes.string.isRequired, - tooltipText: PropTypes.string.isRequired, - isDisabled: PropTypes.bool, - data: dataPropTypes, -}); - -const transactionsDraftPropTypes = PropTypes.shape({ - taxRate: taxRatePropTypes, - taxAmount: PropTypes.number, -}); - -const taxRateDefaultProps = { - text: '', - keyForList: '', - searchText: '', - tooltipText: '', - isDisabled: false, - data: {}, -}; - -const transactionsDraftDefaultProps = { - taxRate: taxRateDefaultProps, - taxAmount: 0, -}; - -export {transactionsDraftPropTypes, transactionsDraftDefaultProps}; diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 8431ededcb56..8bbaf1c9305c 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -2,7 +2,6 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React from 'react'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; import type {PersonalDetails} from '@src/types/onyx'; type CurrentUserPersonalDetails = PersonalDetails | Record<string, never>; @@ -13,15 +12,6 @@ type HOCProps = { type WithCurrentUserPersonalDetailsProps = HOCProps; -// TODO: remove when all components that use it will be migrated to TS -const withCurrentUserPersonalDetailsPropTypes = { - currentUserPersonalDetails: personalDetailsPropType, -}; - -const withCurrentUserPersonalDetailsDefaultProps: HOCProps = { - currentUserPersonalDetails: {}, -}; - export default function <TProps extends WithCurrentUserPersonalDetailsProps, TRef>( WrappedComponent: ComponentType<TProps & RefAttributes<TRef>>, ): ComponentType<Omit<TProps, keyof HOCProps> & RefAttributes<TRef>> { @@ -42,5 +32,4 @@ export default function <TProps extends WithCurrentUserPersonalDetailsProps, TRe return React.forwardRef(WithCurrentUserPersonalDetails); } -export {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps}; export type {WithCurrentUserPersonalDetailsProps, CurrentUserPersonalDetails}; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index fb18d77c779b..cddb2c371a60 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -15,7 +15,6 @@ import type { import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -64,7 +63,7 @@ type PersonalAddress = { function clearPlaid(): Promise<void | void[]> { Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, ''); Onyx.set(ONYXKEYS.PLAID_CURRENT_EVENT, null); - return Onyx.set(ONYXKEYS.PLAID_DATA, PlaidDataProps.plaidDataDefaultProps); + return Onyx.set(ONYXKEYS.PLAID_DATA, CONST.PLAID.DEFAULT_DATA); } function openPlaidView() { diff --git a/src/libs/actions/Plaid.ts b/src/libs/actions/Plaid.ts index 7f63b3920e01..e44e72579819 100644 --- a/src/libs/actions/Plaid.ts +++ b/src/libs/actions/Plaid.ts @@ -3,7 +3,7 @@ import * as API from '@libs/API'; import type {OpenPlaidBankAccountSelectorParams, OpenPlaidBankLoginParams} from '@libs/API/parameters'; import {READ_COMMANDS} from '@libs/API/types'; import getPlaidLinkTokenParameters from '@libs/getPlaidLinkTokenParameters'; -import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; /** @@ -24,7 +24,7 @@ function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) { { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.PLAID_DATA, - value: {...PlaidDataProps.plaidDataDefaultProps, isLoading: true}, + value: {...CONST.PLAID.DEFAULT_DATA, isLoading: true}, }, { onyxMethod: Onyx.METHOD.SET, diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts index 41fa0a4ccbd0..3ad8b9ffe599 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts @@ -3,8 +3,6 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import {WRITE_COMMANDS} from '@libs/API/types'; import {getDefaultCompanyWebsite} from '@libs/BankAccountUtils'; -import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; @@ -71,7 +69,7 @@ function resetFreePlanBankAccount(bankAccountID: number | undefined, session: On { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.PLAID_DATA, - value: PlaidDataProps.plaidDataDefaultProps, + value: CONST.PLAID.DEFAULT_DATA, }, { onyxMethod: Onyx.METHOD.SET, @@ -82,7 +80,7 @@ function resetFreePlanBankAccount(bankAccountID: number | undefined, session: On { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: ReimbursementAccountProps.reimbursementAccountDefaultProps, + value: CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA, }, { onyxMethod: Onyx.METHOD.SET, diff --git a/src/pages/EnablePayments/userWalletPropTypes.js b/src/pages/EnablePayments/userWalletPropTypes.js deleted file mode 100644 index 53332479d4ec..000000000000 --- a/src/pages/EnablePayments/userWalletPropTypes.js +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -/** User's wallet information */ -export default PropTypes.shape({ - /** The user's available wallet balance */ - availableBalance: PropTypes.number, - - /** The user's current wallet balance */ - currentBalance: PropTypes.number, - - /** What step in the activation flow are we on? */ - currentStep: PropTypes.string, - - /** Error code returned by the server */ - errorCode: PropTypes.string, - - /** If we should show the FailedKYC view after the user submitted their info with a non fixable error */ - shouldShowFailedKYC: PropTypes.bool, - - /** Status of wallet - e.g. SILVER or GOLD */ - tierName: PropTypes.string, - - /** Whether the kyc is pending and is yet to be confirmed */ - isPendingOnfidoResult: PropTypes.bool, - - /** The wallet's programID, used to show the correct terms. */ - walletProgramID: PropTypes.string, - - /** Whether the user has failed Onfido completely */ - hasFailedOnfido: PropTypes.bool, -}); diff --git a/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx index 4aec290a4da9..f3ba4c07acf0 100644 --- a/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/BankInfo/substeps/Confirmation.tsx @@ -31,7 +31,7 @@ type ConfirmationOnyxProps = { type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; -const BANK_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX.BANK_ACCOUNT; +const BANK_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT.SUBSTEP_INDEX.BANK_ACCOUNT; function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext, onMove}: ConfirmationProps) { const {translate} = useLocalize(); diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx index 939c52bfed27..13aa2b4056bc 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO.tsx @@ -29,7 +29,7 @@ type ConfirmationUBOOnyxProps = { }; type ConfirmationUBOProps = SubStepProps & ConfirmationUBOOnyxProps & {beneficialOwnerBeingModifiedID: string}; -const UBO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX.UBO; +const UBO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT.SUBSTEP_INDEX.UBO; function ConfirmationUBO({reimbursementAccount, reimbursementAccountDraft, onNext, onMove, beneficialOwnerBeingModifiedID}: ConfirmationUBOProps) { const {translate} = useLocalize(); diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx index a5b839118edc..6311a63a4059 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/ConfirmationBusiness.tsx @@ -35,7 +35,7 @@ type ConfirmationBusinessProps = ConfirmationBusinessOnyxProps & SubStepProps; type States = keyof typeof COMMON_CONST.STATES; const BUSINESS_INFO_STEP_KEYS = INPUT_IDS.BUSINESS_INFO_STEP; -const BUSINESS_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX.BUSINESS_INFO; +const BUSINESS_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT.SUBSTEP_INDEX.BUSINESS_INFO; const validate = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>): FormInputErrors<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM> => { const errors = ValidationUtils.getFieldRequiredErrors(values, [BUSINESS_INFO_STEP_KEYS.HAS_NO_CONNECTION_TO_CANNABIS]); diff --git a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx index 0e11aff395de..af1f081cc3da 100644 --- a/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/PersonalInfo/substeps/Confirmation.tsx @@ -32,7 +32,7 @@ type ConfirmationOnyxProps = { type ConfirmationProps = ConfirmationOnyxProps & SubStepProps; const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; -const PERSONAL_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX.PERSONAL_INFO; +const PERSONAL_INFO_STEP_INDEXES = CONST.REIMBURSEMENT_ACCOUNT.SUBSTEP_INDEX.PERSONAL_INFO; function Confirmation({reimbursementAccount, reimbursementAccountDraft, onNext, onMove}: ConfirmationProps) { const {translate} = useLocalize(); diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 5950cc796e48..eff403a66637 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -40,7 +40,6 @@ import CompanyStep from './CompanyStep'; import ConnectBankAccount from './ConnectBankAccount/ConnectBankAccount'; import ContinueBankAccountSetup from './ContinueBankAccountSetup'; import EnableBankAccount from './EnableBankAccount/EnableBankAccount'; -import * as ReimbursementAccountProps from './reimbursementAccountPropTypes'; import RequestorStep from './RequestorStep'; type ReimbursementAccountOnyxProps = { @@ -204,7 +203,7 @@ function ReimbursementAccountPage({ the full `reimbursementAccount` data from the server. This logic is handled within the useEffect hook, which acts similarly to `componentDidUpdate` when the `reimbursementAccount` dependency changes. */ - const [hasACHDataBeenLoaded, setHasACHDataBeenLoaded] = useState(reimbursementAccount !== ReimbursementAccountProps.reimbursementAccountDefaultProps); + const [hasACHDataBeenLoaded, setHasACHDataBeenLoaded] = useState(reimbursementAccount !== CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA); const [shouldShowContinueSetupButton, setShouldShowContinueSetupButton] = useState(hasACHDataBeenLoaded ? getShouldShowContinueSetupButtonInitialValue() : false); const [isReimbursementAccountLoading, setIsReimbursementAccountLoading] = useState(true); @@ -257,7 +256,7 @@ function ReimbursementAccountPage({ } if (!hasACHDataBeenLoaded) { - if (reimbursementAccount !== ReimbursementAccountProps.reimbursementAccountDefaultProps && isReimbursementAccountLoading === false) { + if (reimbursementAccount !== CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA && isReimbursementAccountLoading === false) { setShouldShowContinueSetupButton(getShouldShowContinueSetupButtonInitialValue()); setHasACHDataBeenLoaded(true); } diff --git a/src/pages/ReimbursementAccount/plaidDataPropTypes.js b/src/pages/ReimbursementAccount/plaidDataPropTypes.js deleted file mode 100644 index bd7b68b67d09..000000000000 --- a/src/pages/ReimbursementAccount/plaidDataPropTypes.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; - -const plaidDataPropTypes = PropTypes.shape({ - /** Whether we are fetching the bank accounts from the API */ - isLoading: PropTypes.bool, - - /** Any additional error message to show */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Name of the bank */ - bankName: PropTypes.string, - - /** Access token returned by Plaid once the user has logged into their bank. This token can be used along with internal credentials to query for Plaid Balance or Assets */ - plaidAccessToken: PropTypes.string, - - /** List of plaid bank accounts */ - bankAccounts: PropTypes.arrayOf( - PropTypes.shape({ - /** Masked account number */ - accountNumber: PropTypes.string, - - /** Name of account */ - addressName: PropTypes.string, - - /** Is the account a savings account? */ - isSavings: PropTypes.bool, - - /** Unique identifier for this account in Plaid */ - plaidAccountID: PropTypes.string, - - /** Routing number for the account */ - routingNumber: PropTypes.string, - - /** last 4 digits of the account number */ - mask: PropTypes.string, - - /** Plaid access token, used to then retrieve Assets and Balances */ - plaidAccessToken: PropTypes.string, - }), - ), -}); - -const plaidDataDefaultProps = { - bankName: '', - plaidAccessToken: '', - bankAccounts: [], - isLoading: false, - error: '', - errors: {}, -}; - -export {plaidDataPropTypes, plaidDataDefaultProps}; diff --git a/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js b/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js deleted file mode 100644 index 8e94e2ad8f30..000000000000 --- a/src/pages/ReimbursementAccount/reimbursementAccountPropTypes.js +++ /dev/null @@ -1,47 +0,0 @@ -import PropTypes from 'prop-types'; -import BankAccount from '@libs/models/BankAccount'; - -const reimbursementAccountPropTypes = PropTypes.shape({ - /** Whether we are loading the data via the API */ - isLoading: PropTypes.bool, - - /** A date that indicates the user has been throttled */ - throttledDate: PropTypes.string, - - /** Additional data for the account in setup */ - achData: PropTypes.shape({ - /** Step of the setup flow that we are on. Determines which view is presented. */ - currentStep: PropTypes.string, - - /** Bank account state */ - state: PropTypes.string, - - /** Bank account ID of the VBA that we are validating is required */ - bankAccountID: PropTypes.number, - }), - - /** Disable validation button if max attempts exceeded */ - maxAttemptsReached: PropTypes.bool, - - /** Alert message to display above submit button */ - error: PropTypes.string, - - /** Which field needs attention? */ - errorFields: PropTypes.objectOf(PropTypes.oneOfType([PropTypes.bool, PropTypes.array])), - - /** Any additional error message to show */ - errors: PropTypes.objectOf(PropTypes.string), -}); - -const reimbursementAccountDefaultProps = { - achData: { - state: BankAccount.STATE.SETUP, - }, - isLoading: false, - errorFields: {}, - errors: {}, - maxAttemptsReached: false, - shouldShowResetModal: false, -}; - -export {reimbursementAccountPropTypes, reimbursementAccountDefaultProps}; diff --git a/src/pages/ValidateLoginPage/validateLinkPropTypes.js b/src/pages/ValidateLoginPage/validateLinkPropTypes.js deleted file mode 100644 index f3db0e5dc5c2..000000000000 --- a/src/pages/ValidateLoginPage/validateLinkPropTypes.js +++ /dev/null @@ -1,24 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = PropTypes.shape({ - // The name of the route - name: PropTypes.string, - - // Unique key associated with the route - key: PropTypes.string, - - // Each parameter passed via the URL - params: PropTypes.shape({ - // AccountID associated with the validation link - accountID: PropTypes.string, - - // Validation code associated with the validation link - validateCode: PropTypes.string, - }), -}); - -const defaultProps = { - params: {}, -}; - -export {propTypes, defaultProps}; diff --git a/src/pages/home/report/reportActionSourcePropType.js b/src/pages/home/report/reportActionSourcePropType.js deleted file mode 100644 index 0ad9662eb693..000000000000 --- a/src/pages/home/report/reportActionSourcePropType.js +++ /dev/null @@ -1,3 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOf(['Chronos', 'email', 'ios', 'android', 'web', 'email', '']); diff --git a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js deleted file mode 100644 index f69e2b122c24..000000000000 --- a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js +++ /dev/null @@ -1,29 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import CONST from '@src/CONST'; - -export default PropTypes.shape({ - /** Route specific parameters used on this screen via route :iouType/new/category/:reportID? */ - params: PropTypes.shape({ - /** What action is being performed, ie. create, edit */ - action: PropTypes.oneOf(_.values(CONST.IOU.ACTION)), - - /** The type of IOU report, i.e. split, request, send, track */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)).isRequired, - - /** The ID of the transaction being configured */ - transactionID: PropTypes.string.isRequired, - - /** The report ID of the IOU */ - reportID: PropTypes.string.isRequired, - - /** Index of the waypoint being edited */ - pageIndex: PropTypes.string, - - /** A path to go to when the user presses the back button */ - backTo: PropTypes.string, - - /** Indicates which tag list index was selected */ - tagIndex: PropTypes.string, - }), -}); diff --git a/src/pages/iouReportPropTypes.js b/src/pages/iouReportPropTypes.js deleted file mode 100644 index 284f0915dbbc..000000000000 --- a/src/pages/iouReportPropTypes.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** The report ID of the IOU */ - reportID: PropTypes.string, - - /** The report ID of the chat associated with the IOU */ - chatReportID: PropTypes.string, - - /** The total amount in cents */ - total: PropTypes.number, - - /** The owner of the IOUReport */ - ownerAccountID: PropTypes.number, - - /** The currency of the IOUReport */ - currency: PropTypes.string, -}); diff --git a/src/pages/nextStepPropTypes.js b/src/pages/nextStepPropTypes.js deleted file mode 100644 index 4bf4d265ddef..000000000000 --- a/src/pages/nextStepPropTypes.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types'; - -const messagePropType = PropTypes.shape({ - text: PropTypes.string, - type: PropTypes.string, - action: PropTypes.string, -}); - -export default PropTypes.shape({ - /** The message parts of the next step */ - message: PropTypes.arrayOf(messagePropType), - - /** The title for the next step */ - title: PropTypes.string, - - /** Whether the user should take some sort of action in order to unblock the report */ - requiresUserAction: PropTypes.bool, - - /** The type of next step */ - type: PropTypes.oneOf(['neutral', 'alert', null]), - - /** If the "Undo submit" button should be visible */ - showUndoSubmit: PropTypes.bool, - - /** Deprecated - If the next step should be displayed on mobile, related to OldApp */ - showForMobile: PropTypes.bool, - - /** If the next step should be displayed at the expense level */ - showForExpense: PropTypes.bool, - - /** An optional alternate message to display on expenses instead of what is provided in the "message" field */ - expenseMessage: PropTypes.arrayOf(messagePropType), - - /** The next person in the approval chain of the report */ - nextReceiver: PropTypes.string, - - /** An array of buttons to be displayed next to the next step */ - buttons: PropTypes.objectOf( - PropTypes.shape({ - text: PropTypes.string, - tooltip: PropTypes.string, - disabled: PropTypes.bool, - hidden: PropTypes.bool, - // eslint-disable-next-line react/forbid-prop-types - data: PropTypes.object, - }), - ), -}); diff --git a/src/pages/personalDetailsPropType.js b/src/pages/personalDetailsPropType.js deleted file mode 100644 index 37cab8e56abd..000000000000 --- a/src/pages/personalDetailsPropType.js +++ /dev/null @@ -1,53 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - // First name of the current user from their personal details - firstName: PropTypes.string, - - // Last name of the current user from their personal details - lastName: PropTypes.string, - - // Display name of the current user from their personal details - displayName: PropTypes.string, - - // Avatar URL of the current user from their personal details - avatar: PropTypes.string, - - // Flag to set when Avatar uploading - avatarUploading: PropTypes.bool, - - // accountID of the current user from their personal details - accountID: PropTypes.number, - - // login of the current user from their personal details - login: PropTypes.string, - - // pronouns of the current user from their personal details - pronouns: PropTypes.string, - - // local currency for the user - localCurrencyCode: PropTypes.string, - - // timezone of the current user from their personal details - timezone: PropTypes.shape({ - // Value of selected timezone - selected: PropTypes.string, - - // Whether timezone is automatically set - // TODO: remove string type after backend fix - // Some personal details return 'true' (string) for this value instead of true (boolean) - automatic: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - }), - - // custom status - status: PropTypes.shape({ - // The emoji code of the draft status - emojiCode: PropTypes.string, - - // The text of the draft status - text: PropTypes.string, - - // The timestamp of when the status should be cleared - clearAfter: PropTypes.string, // ISO 8601 format - }), -}); diff --git a/src/pages/policyMemberPropType.js b/src/pages/policyMemberPropType.js deleted file mode 100644 index 22a4d355fbfb..000000000000 --- a/src/pages/policyMemberPropType.js +++ /dev/null @@ -1,15 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** Role of the user in the policy */ - role: PropTypes.string, - - /** - * Errors from api calls on the specific user - * {<timestamp>: 'error message', <timestamp2>: 'error message 2'} - */ - errors: PropTypes.objectOf(PropTypes.string), - - /** Is this action pending? */ - pendingAction: PropTypes.string, -}); diff --git a/src/pages/reportMetadataPropTypes.js b/src/pages/reportMetadataPropTypes.js deleted file mode 100644 index 65ed01952977..000000000000 --- a/src/pages/reportMetadataPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** Are we loading newer report actions? */ - isLoadingNewerReportActions: PropTypes.bool, - - /** Are we loading older report actions? */ - isLoadingOlderReportActions: PropTypes.bool, - - /** Flag to check if the report actions data are loading */ - isLoadingInitialReportActions: PropTypes.bool, -}); diff --git a/src/pages/safeAreaInsetPropTypes.js b/src/pages/safeAreaInsetPropTypes.js deleted file mode 100644 index 9b301463fcae..000000000000 --- a/src/pages/safeAreaInsetPropTypes.js +++ /dev/null @@ -1,10 +0,0 @@ -import PropTypes from 'prop-types'; - -const safeAreaInsetPropTypes = PropTypes.shape({ - top: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, -}); - -export default safeAreaInsetPropTypes; diff --git a/src/pages/settings/Wallet/assignedCardPropTypes.js b/src/pages/settings/Wallet/assignedCardPropTypes.js deleted file mode 100644 index a560c0250388..000000000000 --- a/src/pages/settings/Wallet/assignedCardPropTypes.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types'; -import CONST from '@src/CONST'; - -/** Assigned Card props */ -const assignedCardPropTypes = PropTypes.shape({ - cardID: PropTypes.number, - state: PropTypes.number, - bank: PropTypes.string, - availableSpend: PropTypes.number, - domainName: PropTypes.string, - maskedPan: PropTypes.string, - isVirtual: PropTypes.bool, - fraud: PropTypes.oneOf([CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN, CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL, CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE]), - cardholderFirstName: PropTypes.string, - cardholderLastName: PropTypes.string, -}); - -export default assignedCardPropTypes; diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 4889c1dbe350..58288f213818 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -16,8 +16,8 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import BankAccount from '@libs/models/BankAccount'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; @@ -103,7 +103,7 @@ function WorkspacePageWithSections({ headerText, policy, policyDraft, - reimbursementAccount = ReimbursementAccountProps.reimbursementAccountDefaultProps, + reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA, route, shouldUseScrollView = false, shouldSkipVBBACall = false, diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 9c2926aaaa17..2c06bf208482 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -5,7 +5,6 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {forwardRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import taxPropTypes from '@components/taxPropTypes'; import {translatableTextPropTypes} from '@libs/Localize'; import type { BottomTabNavigatorParamList, @@ -54,6 +53,34 @@ function getPolicyIDFromRoute(route: PolicyRoute): string { return route?.params?.policyID ?? ''; } +const taxPropTypes = PropTypes.shape({ + /** Name of a tax */ + name: PropTypes.string, + + /** The value of a tax */ + value: PropTypes.string, + + /** Whether the tax is disabled */ + isDisabled: PropTypes.bool, +}); + +const taxRatesPropTypes = PropTypes.shape({ + /** Name of the tax */ + name: PropTypes.string, + + /** Default policy tax ID */ + defaultExternalID: PropTypes.string, + + /** Default value of taxes */ + defaultValue: PropTypes.string, + + /** Default foreign policy tax ID */ + foreignTaxDefault: PropTypes.string, + + /** List of tax names and values */ + taxes: PropTypes.objectOf(taxPropTypes), +}); + const policyPropTypes = { /** The policy object for the current route */ policy: PropTypes.shape({ @@ -114,7 +141,7 @@ const policyPropTypes = { }), /** Collection of tax rates attached to a policy */ - taxRates: taxPropTypes, + taxRates: taxRatesPropTypes, }), }; diff --git a/src/types/onyx/PlaidData.ts b/src/types/onyx/PlaidData.ts index 8ec93119cbd8..3a538efe3623 100644 --- a/src/types/onyx/PlaidData.ts +++ b/src/types/onyx/PlaidData.ts @@ -15,6 +15,7 @@ type PlaidData = { bankAccounts?: PlaidBankAccount[]; isLoading?: boolean; + error?: string; errors: OnyxCommon.Errors; }; From eac61eed3efbf58cb46f25dc512a364cd3e8176c Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:50:58 +0530 Subject: [PATCH 236/580] Enable --- .eslintrc.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 23e522c679aa..0cecabdc9cdd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -283,5 +283,11 @@ module.exports = { 'no-restricted-syntax': ['error', 'ForInStatement', 'LabeledStatement', 'WithStatement'], }, }, + { + files: ['en.ts', 'es.ts'], + rules: { + 'rulesdir/use-periods-for-error-messages': 'error', + } + } ], }; From ca0650bdd9156a4896da21e1811e808284226eb9 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Fri, 19 Apr 2024 20:22:44 +0530 Subject: [PATCH 237/580] revert margin changes. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 4 ++-- src/styles/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 7100e1a60842..73539656cd48 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -14,7 +14,7 @@ function ReceiptAuditHeader({notes = [], showAuditMessage = false}: {notes?: str const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('common.verified'); return ( - <View style={[styles.ph5, styles.mb1]}> + <View style={[styles.ph5, styles.mbn1]}> <View style={[styles.flexRow, styles.alignItemsCenter]}> <Text style={[styles.textLabelSupporting]}>{translate('common.receipt')}</Text> {showAuditMessage && ( @@ -36,7 +36,7 @@ function ReceiptAuditHeader({notes = [], showAuditMessage = false}: {notes?: str function ReceiptAuditMessages({notes = []}: {notes?: string[]}) { const styles = useThemeStyles(); - return <View style={[styles.mt1, styles.mb2, styles.ph5, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View>; + return <View style={[styles.mtn1, styles.mb2, styles.ph5, styles.gap1]}>{notes.length > 0 && notes.map((message) => <Text style={[styles.textLabelError]}>{message}</Text>)}</View>; } export {ReceiptAuditHeader, ReceiptAuditMessages}; diff --git a/src/styles/index.ts b/src/styles/index.ts index 85229ffd3fc8..537038d9f2e1 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4296,7 +4296,7 @@ const styles = (theme: ThemeColors) => }, moneyRequestViewImage: { ...spacing.mh5, - ...spacing.mv1, + ...spacing.mv3, overflow: 'hidden', borderWidth: 2, borderColor: theme.cardBG, From 6a18afb5f0b1622f8d7df3d61548aad90bb03aa0 Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Fri, 19 Apr 2024 16:54:31 +0200 Subject: [PATCH 238/580] remove feature wrapper --- .../workspace/AccessOrNotFoundWrapper.tsx | 49 ++++-- .../FeatureEnabledAccessOrNotFoundWrapper.tsx | 74 --------- .../workspace/WorkspaceMoreFeaturesPage.tsx | 5 +- .../accounting/PolicyAccountingPage.tsx | 98 ++++++------ .../qbo/QuickbooksChartOfAccountsPage.tsx | 69 ++++---- .../accounting/qbo/QuickbooksClassesPage.tsx | 83 +++++----- .../qbo/QuickbooksCustomersPage.tsx | 83 +++++----- .../accounting/qbo/QuickbooksImportPage.tsx | 53 +++---- .../qbo/QuickbooksLocationsPage.tsx | 89 +++++------ .../accounting/qbo/QuickbooksTaxesPage.tsx | 65 ++++---- .../categories/CategorySettingsPage.tsx | 106 ++++++------- .../categories/CreateCategoryPage.tsx | 40 +++-- .../workspace/categories/EditCategoryPage.tsx | 44 +++--- .../categories/WorkspaceCategoriesPage.tsx | 122 +++++++-------- .../WorkspaceCategoriesSettingsPage.tsx | 66 ++++---- .../distanceRates/CreateDistanceRatePage.tsx | 66 ++++---- .../PolicyDistanceRateDetailsPage.tsx | 130 ++++++++------- .../PolicyDistanceRateEditPage.tsx | 74 +++++---- .../distanceRates/PolicyDistanceRatesPage.tsx | 120 +++++++------- .../PolicyDistanceRatesSettingsPage.tsx | 74 +++++---- .../members/WorkspaceMemberDetailsPage.tsx | 5 +- .../members/WorkspaceOwnerChangeErrorPage.tsx | 5 +- .../WorkspaceOwnerChangeSuccessPage.tsx | 5 +- .../WorkspaceOwnerChangeWrapperPage.tsx | 5 +- src/pages/workspace/tags/EditTagPage.tsx | 66 ++++---- src/pages/workspace/tags/TagSettingsPage.tsx | 122 +++++++-------- .../workspace/tags/WorkspaceCreateTagPage.tsx | 64 ++++---- .../workspace/tags/WorkspaceEditTagsPage.tsx | 62 ++++---- .../workspace/tags/WorkspaceTagsPage.tsx | 121 +++++++------- .../tags/WorkspaceTagsSettingsPage.tsx | 88 +++++------ src/pages/workspace/taxes/NamePage.tsx | 74 +++++---- src/pages/workspace/taxes/ValuePage.tsx | 80 +++++----- .../taxes/WorkspaceCreateTaxPage.tsx | 104 ++++++------ .../workspace/taxes/WorkspaceEditTaxPage.tsx | 148 +++++++++--------- .../workspace/taxes/WorkspaceTaxesPage.tsx | 110 +++++++------ .../WorkspaceTaxesSettingsCustomTaxName.tsx | 72 +++++---- .../WorkspaceTaxesSettingsForeignCurrency.tsx | 50 +++--- .../taxes/WorkspaceTaxesSettingsPage.tsx | 60 ++++--- ...orkspaceTaxesSettingsWorkspaceCurrency.tsx | 50 +++--- .../WorkspaceAutoReportingFrequencyPage.tsx | 6 +- ...orkspaceAutoReportingMonthlyOffsetPage.tsx | 6 +- .../WorkspaceWorkflowsApproverPage.tsx | 6 +- .../workflows/WorkspaceWorkflowsPage.tsx | 6 +- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 5 +- 44 files changed, 1361 insertions(+), 1469 deletions(-) delete mode 100644 src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 333aba514168..29896eb5b6d0 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -2,6 +2,7 @@ import React, {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -10,6 +11,8 @@ import * as Policy from '@userActions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; +import type {PolicyFeatureName} from '@src/types/onyx/Policy'; +import callOrReturn from '@src/types/utils/callOrReturn'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; const POLICY_ACCESS_VARIANTS = { @@ -36,38 +39,64 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { /** Defines which types of access should be verified */ accessVariants?: PolicyAccessVariant[]; + + /** The current feature name that the user tries to get access to */ + featureName?: PolicyFeatureName; }; -function AccessOrNotFoundWrapper({accessVariants = ['ADMIN', 'PAID'], ...props}: AccessOrNotFoundWrapperProps) { - const isPolicyIDInRoute = !!props.policyID?.length; +type PageNotFoundFallackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID'> & {showFullScreenFallback: boolean}; + +function PageNotFoundFallback({policyID, showFullScreenFallback}: PageNotFoundFallackProps) { + return showFullScreenFallback ? ( + <FullPageNotFoundView + shouldShow + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} + shouldForceFullScreen + /> + ) : ( + <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(policyID))} /> + ); +} + +function AccessOrNotFoundWrapper({accessVariants = [], ...props}: AccessOrNotFoundWrapperProps) { + const {policy, policyID, featureName, isLoadingReportData} = props; + + const isPolicyIDInRoute = !!policyID?.length; useEffect(() => { - if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { + if (!isPolicyIDInRoute || !isEmptyObject(policy)) { // If the workspace is not required or is already loaded, we don't need to call the API return; } - Policy.openWorkspace(props.policyID, []); + Policy.openWorkspace(policyID, []); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPolicyIDInRoute, props.policyID]); + }, [isPolicyIDInRoute, policyID]); - const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + const shouldShowFullScreenLoadingIndicator = isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); + const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true; const pageUnaccessible = accessVariants.reduce((acc, variant) => { const accessFunction = POLICY_ACCESS_VARIANTS[variant]; - return acc || accessFunction(props.policy); + return acc || accessFunction(policy); }, false); - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || pageUnaccessible; + + const shouldShowNotFoundPage = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id || pageUnaccessible || !isFeatureEnabled; if (shouldShowFullScreenLoadingIndicator) { return <FullscreenLoadingIndicator />; } if (shouldShowNotFoundPage) { - return <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; + return ( + <PageNotFoundFallback + policyID={policyID} + showFullScreenFallback={!isFeatureEnabled} + /> + ); } - return typeof props.children === 'function' ? props.children(props) : props.children; + return callOrReturn(props.children, props); } export default withOnyx<AccessOrNotFoundWrapperProps, AccessOrNotFoundWrapperOnyxProps>({ diff --git a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx deleted file mode 100644 index 3bcdc1fe3303..000000000000 --- a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* eslint-disable rulesdir/no-negated-variables */ -import React, {useEffect} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as Policy from '@userActions/Policy'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {PolicyFeatureName} from '@src/types/onyx/Policy'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -type FeatureEnabledAccessOrNotFoundOnyxProps = { - /** The report currently being looked at */ - policy: OnyxEntry<OnyxTypes.Policy>; - - /** Indicated whether the report data is loading */ - isLoadingReportData: OnyxEntry<boolean>; -}; - -type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFoundOnyxProps & { - /** The children to render */ - children: ((props: FeatureEnabledAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; - - /** The report currently being looked at */ - policyID: string; - - /** The current feature name that the user tries to get access */ - featureName: PolicyFeatureName; -}; - -function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNotFoundComponentProps) { - const isPolicyIDInRoute = !!props.policyID?.length; - const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); - - useEffect(() => { - if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { - // If the workspace is not required or is already loaded, we don't need to call the API - return; - } - - Policy.openWorkspace(props.policyID, []); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPolicyIDInRoute, props.policyID]); - - if (shouldShowFullScreenLoadingIndicator) { - return <FullscreenLoadingIndicator />; - } - - if (shouldShowNotFoundPage) { - return ( - <FullPageNotFoundView - shouldShow={shouldShowNotFoundPage} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} - shouldForceFullScreen - /> - ); - } - - return typeof props.children === 'function' ? props.children(props) : props.children; -} - -export default withOnyx<FeatureEnabledAccessOrNotFoundComponentProps, FeatureEnabledAccessOrNotFoundOnyxProps>({ - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, - }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - }, -})(FeatureEnabledAccessOrNotFoundComponent); diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 46603c0712de..e904e6848ff2 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -195,7 +195,10 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro ); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + > <ScreenWrapper includeSafeAreaPaddingBottom={false} style={[styles.defaultModalContainer]} diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index c6b91ec37e97..8777bd9cd263 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -21,7 +21,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import {removePolicyConnection} from '@libs/actions/connections'; import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import type {AnchorPosition} from '@styles/index'; @@ -174,56 +173,55 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting ]; return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + > + <ScreenWrapper + testID={PolicyAccountingPage.displayName} + includeSafeAreaPaddingBottom={false} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - testID={PolicyAccountingPage.displayName} - includeSafeAreaPaddingBottom={false} - shouldShowOfflineIndicatorInWideScreen - > - <HeaderWithBackButton - title={translate('workspace.common.accounting')} - shouldShowBackButton={isSmallScreenWidth} - icon={Illustrations.Accounting} - shouldShowThreeDotsButton - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} - threeDotsMenuItems={headerThreeDotsMenuItems} - /> - <ScrollView contentContainerStyle={styles.pt3}> - <View style={[styles.flex1, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}> - <Section - title={translate('workspace.accounting.title')} - subtitle={translate('workspace.accounting.subtitle')} - isCentralPane - subtitleMuted - titleStyles={styles.accountSettingsSectionTitle} - childrenStyles={styles.pt5} - > - <MenuItemList - menuItems={menuItems} - shouldUseSingleExecution - /> - </Section> - </View> - </ScrollView> - <ConfirmModal - title={translate('workspace.accounting.disconnectTitle')} - isVisible={isDisconnectModalOpen} - onConfirm={() => { - removePolicyConnection(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO); - setIsDisconnectModalOpen(false); - }} - onCancel={() => setIsDisconnectModalOpen(false)} - prompt={translate('workspace.accounting.disconnectPrompt')} - confirmText={translate('workspace.accounting.disconnect')} - cancelText={translate('common.cancel')} - danger - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <HeaderWithBackButton + title={translate('workspace.common.accounting')} + shouldShowBackButton={isSmallScreenWidth} + icon={Illustrations.Accounting} + shouldShowThreeDotsButton + threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} + threeDotsMenuItems={headerThreeDotsMenuItems} + /> + <ScrollView contentContainerStyle={styles.pt3}> + <View style={[styles.flex1, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}> + <Section + title={translate('workspace.accounting.title')} + subtitle={translate('workspace.accounting.subtitle')} + isCentralPane + subtitleMuted + titleStyles={styles.accountSettingsSectionTitle} + childrenStyles={styles.pt5} + > + <MenuItemList + menuItems={menuItems} + shouldUseSingleExecution + /> + </Section> + </View> + </ScrollView> + <ConfirmModal + title={translate('workspace.accounting.disconnectTitle')} + isVisible={isDisconnectModalOpen} + onConfirm={() => { + removePolicyConnection(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO); + setIsDisconnectModalOpen(false); + }} + onCancel={() => setIsDisconnectModalOpen(false)} + prompt={translate('workspace.accounting.disconnectPrompt')} + confirmText={translate('workspace.accounting.disconnect')} + cancelText={translate('common.cancel')} + danger + /> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage.tsx index 8401d0deb9ab..053df1d3e585 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksChartOfAccountsPage.tsx @@ -10,7 +10,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import variables from '@styles/variables'; @@ -27,46 +26,42 @@ function QuickbooksChartOfAccountsPage({policy}: WithPolicyProps) { <AccessOrNotFoundWrapper accessVariants={['ADMIN']} policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={QuickbooksChartOfAccountsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={QuickbooksChartOfAccountsPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.qbo.accounts')} /> - <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> - <Text style={styles.pb5}>{translate('workspace.qbo.accountsDescription')}</Text> - <View style={[styles.flexRow, styles.mb2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <View style={styles.flex1}> - <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.accountsSwitchTitle')}</Text> - </View> - <OfflineWithFeedback pendingAction={pendingFields?.enableNewCategories}> - <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> - <Switch - accessibilityLabel={translate('workspace.qbo.accounts')} - isOn={isSwitchOn} - onToggle={() => - Connections.updatePolicyConnectionConfig( - policyID, - CONST.POLICY.CONNECTIONS.NAME.QBO, - CONST.QUICKBOOKS_IMPORTS.ENABLE_NEW_CATEGORIES, - isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, - ) - } - /> - </View> - </OfflineWithFeedback> - </View> + <HeaderWithBackButton title={translate('workspace.qbo.accounts')} /> + <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> + <Text style={styles.pb5}>{translate('workspace.qbo.accountsDescription')}</Text> + <View style={[styles.flexRow, styles.mb2, styles.alignItemsCenter, styles.justifyContentBetween]}> <View style={styles.flex1}> - <Text style={styles.mutedTextLabel}>{translate('workspace.qbo.accountsSwitchDescription')}</Text> + <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.accountsSwitchTitle')}</Text> </View> - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <OfflineWithFeedback pendingAction={pendingFields?.enableNewCategories}> + <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> + <Switch + accessibilityLabel={translate('workspace.qbo.accounts')} + isOn={isSwitchOn} + onToggle={() => + Connections.updatePolicyConnectionConfig( + policyID, + CONST.POLICY.CONNECTIONS.NAME.QBO, + CONST.QUICKBOOKS_IMPORTS.ENABLE_NEW_CATEGORIES, + isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, + ) + } + /> + </View> + </OfflineWithFeedback> + </View> + <View style={styles.flex1}> + <Text style={styles.mutedTextLabel}>{translate('workspace.qbo.accountsSwitchDescription')}</Text> + </View> + </ScrollView> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksClassesPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksClassesPage.tsx index c16ba8b687dc..d0407bd264e3 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksClassesPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksClassesPage.tsx @@ -11,7 +11,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import variables from '@styles/variables'; @@ -29,53 +28,49 @@ function QuickbooksClassesPage({policy}: WithPolicyProps) { <AccessOrNotFoundWrapper accessVariants={['ADMIN']} policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={QuickbooksClassesPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={QuickbooksClassesPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.qbo.classes')} /> - <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> - <Text style={styles.pb5}>{translate('workspace.qbo.classesDescription')}</Text> - <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}> - <View style={styles.flex1}> - <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.import')}</Text> - </View> - <OfflineWithFeedback pendingAction={pendingFields?.syncClasses}> - <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> - <Switch - accessibilityLabel={translate('workspace.qbo.classes')} - isOn={isSwitchOn} - onToggle={() => - Connections.updatePolicyConnectionConfig( - policyID, - CONST.POLICY.CONNECTIONS.NAME.QBO, - CONST.QUICKBOOKS_IMPORTS.SYNC_CLASSES, - isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, - ) - } - /> - </View> - </OfflineWithFeedback> + <HeaderWithBackButton title={translate('workspace.qbo.classes')} /> + <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> + <Text style={styles.pb5}>{translate('workspace.qbo.classesDescription')}</Text> + <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}> + <View style={styles.flex1}> + <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.import')}</Text> </View> - {isSwitchOn && ( - <OfflineWithFeedback> - <MenuItemWithTopDescription - interactive={false} - title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')} - description={translate('workspace.qbo.displayedAs')} - wrapperStyle={styles.sectionMenuItemTopDescription} + <OfflineWithFeedback pendingAction={pendingFields?.syncClasses}> + <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> + <Switch + accessibilityLabel={translate('workspace.qbo.classes')} + isOn={isSwitchOn} + onToggle={() => + Connections.updatePolicyConnectionConfig( + policyID, + CONST.POLICY.CONNECTIONS.NAME.QBO, + CONST.QUICKBOOKS_IMPORTS.SYNC_CLASSES, + isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, + ) + } /> - </OfflineWithFeedback> - )} - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + </View> + {isSwitchOn && ( + <OfflineWithFeedback> + <MenuItemWithTopDescription + interactive={false} + title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')} + description={translate('workspace.qbo.displayedAs')} + wrapperStyle={styles.sectionMenuItemTopDescription} + /> + </OfflineWithFeedback> + )} + </ScrollView> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksCustomersPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksCustomersPage.tsx index 05c4a5608e75..ac84448f8796 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksCustomersPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksCustomersPage.tsx @@ -11,7 +11,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import variables from '@styles/variables'; @@ -28,53 +27,49 @@ function QuickbooksCustomersPage({policy}: WithPolicyProps) { <AccessOrNotFoundWrapper accessVariants={['ADMIN']} policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={QuickbooksCustomersPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={QuickbooksCustomersPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.qbo.customers')} /> - <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> - <Text style={styles.pb5}>{translate('workspace.qbo.customersDescription')}</Text> - <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}> - <View style={styles.flex1}> - <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.import')}</Text> - </View> - <OfflineWithFeedback pendingAction={pendingFields?.syncCustomers}> - <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> - <Switch - accessibilityLabel={translate('workspace.qbo.customers')} - isOn={isSwitchOn} - onToggle={() => - Connections.updatePolicyConnectionConfig( - policyID, - CONST.POLICY.CONNECTIONS.NAME.QBO, - CONST.QUICKBOOKS_IMPORTS.SYNC_CUSTOMERS, - isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, - ) - } - /> - </View> - </OfflineWithFeedback> + <HeaderWithBackButton title={translate('workspace.qbo.customers')} /> + <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> + <Text style={styles.pb5}>{translate('workspace.qbo.customersDescription')}</Text> + <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}> + <View style={styles.flex1}> + <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.import')}</Text> </View> - {isSwitchOn && ( - <OfflineWithFeedback> - <MenuItemWithTopDescription - interactive={false} - title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')} - description={translate('workspace.qbo.displayedAs')} - wrapperStyle={styles.sectionMenuItemTopDescription} + <OfflineWithFeedback pendingAction={pendingFields?.syncCustomers}> + <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> + <Switch + accessibilityLabel={translate('workspace.qbo.customers')} + isOn={isSwitchOn} + onToggle={() => + Connections.updatePolicyConnectionConfig( + policyID, + CONST.POLICY.CONNECTIONS.NAME.QBO, + CONST.QUICKBOOKS_IMPORTS.SYNC_CUSTOMERS, + isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, + ) + } /> - </OfflineWithFeedback> - )} - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + </View> + {isSwitchOn && ( + <OfflineWithFeedback> + <MenuItemWithTopDescription + interactive={false} + title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')} + description={translate('workspace.qbo.displayedAs')} + wrapperStyle={styles.sectionMenuItemTopDescription} + /> + </OfflineWithFeedback> + )} + </ScrollView> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksImportPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksImportPage.tsx index 04bc26e9ff8c..8d7fc83cde78 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksImportPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksImportPage.tsx @@ -9,7 +9,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import CONST from '@src/CONST'; @@ -71,36 +70,32 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { <AccessOrNotFoundWrapper accessVariants={['ADMIN']} policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={QuickbooksImportPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={QuickbooksImportPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.qbo.import')} /> - <ScrollView contentContainerStyle={styles.pb2}> - <Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.importDescription')}</Text> - {sections.map((section) => ( - <OfflineWithFeedback - key={section.description} - pendingAction={section.pendingAction} - > - <MenuItemWithTopDescription - title={quickbooksOnlineConfigTitles[`${section.title ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE}`]} - description={section.description} - shouldShowRightIcon - onPress={section.action} - brickRoadIndicator={section.hasError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - /> - </OfflineWithFeedback> - ))} - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <HeaderWithBackButton title={translate('workspace.qbo.import')} /> + <ScrollView contentContainerStyle={styles.pb2}> + <Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.importDescription')}</Text> + {sections.map((section) => ( + <OfflineWithFeedback + key={section.description} + pendingAction={section.pendingAction} + > + <MenuItemWithTopDescription + title={quickbooksOnlineConfigTitles[`${section.title ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE}`]} + description={section.description} + shouldShowRightIcon + onPress={section.action} + brickRoadIndicator={section.hasError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + </OfflineWithFeedback> + ))} + </ScrollView> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksLocationsPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksLocationsPage.tsx index 22d689063b0a..f36e1c950b64 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksLocationsPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksLocationsPage.tsx @@ -11,7 +11,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import variables from '@styles/variables'; @@ -29,56 +28,52 @@ function QuickbooksLocationsPage({policy}: WithPolicyProps) { <AccessOrNotFoundWrapper accessVariants={['ADMIN']} policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={QuickbooksLocationsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={QuickbooksLocationsPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.qbo.locations')} /> - <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> - <Text style={styles.pb5}>{translate('workspace.qbo.locationsDescription')}</Text> - <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}> - <View style={styles.flex1}> - <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.import')}</Text> - </View> - <OfflineWithFeedback pendingAction={pendingFields?.syncLocations}> - <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> - <Switch - accessibilityLabel={translate('workspace.qbo.locations')} - isOn={isSwitchOn} - onToggle={() => - Connections.updatePolicyConnectionConfig( - policyID, - CONST.POLICY.CONNECTIONS.NAME.QBO, - CONST.QUICKBOOKS_IMPORTS.SYNC_LOCATIONS, - isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, - ) - } - /> - </View> - </OfflineWithFeedback> + <HeaderWithBackButton title={translate('workspace.qbo.locations')} /> + <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> + <Text style={styles.pb5}>{translate('workspace.qbo.locationsDescription')}</Text> + <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}> + <View style={styles.flex1}> + <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.import')}</Text> </View> - {isSwitchOn && ( - <OfflineWithFeedback> - <MenuItemWithTopDescription - interactive={false} - title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')} - description={translate('workspace.qbo.displayedAs')} - wrapperStyle={styles.sectionMenuItemTopDescription} + <OfflineWithFeedback pendingAction={pendingFields?.syncLocations}> + <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> + <Switch + accessibilityLabel={translate('workspace.qbo.locations')} + isOn={isSwitchOn} + onToggle={() => + Connections.updatePolicyConnectionConfig( + policyID, + CONST.POLICY.CONNECTIONS.NAME.QBO, + CONST.QUICKBOOKS_IMPORTS.SYNC_LOCATIONS, + isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, + ) + } /> - </OfflineWithFeedback> - )} - <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.gap2, styles.mt1]}> - <Text style={styles.mutedTextLabel}>{translate('workspace.qbo.locationsAdditionalDescription')}</Text> - </View> - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + </View> + {isSwitchOn && ( + <OfflineWithFeedback> + <MenuItemWithTopDescription + interactive={false} + title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')} + description={translate('workspace.qbo.displayedAs')} + wrapperStyle={styles.sectionMenuItemTopDescription} + /> + </OfflineWithFeedback> + )} + <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.gap2, styles.mt1]}> + <Text style={styles.mutedTextLabel}>{translate('workspace.qbo.locationsAdditionalDescription')}</Text> + </View> + </ScrollView> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/QuickbooksTaxesPage.tsx b/src/pages/workspace/accounting/qbo/QuickbooksTaxesPage.tsx index 8745876f33ab..b30af0912544 100644 --- a/src/pages/workspace/accounting/qbo/QuickbooksTaxesPage.tsx +++ b/src/pages/workspace/accounting/qbo/QuickbooksTaxesPage.tsx @@ -10,7 +10,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import variables from '@styles/variables'; @@ -26,43 +25,39 @@ function QuickbooksTaxesPage({policy}: WithPolicyProps) { <AccessOrNotFoundWrapper accessVariants={['ADMIN']} policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={QuickbooksTaxesPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={QuickbooksTaxesPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.qbo.taxes')} /> - <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> - <Text style={styles.pb5}>{translate('workspace.qbo.taxesDescription')}</Text> - <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}> - <View style={styles.flex1}> - <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.import')}</Text> - </View> - <OfflineWithFeedback pendingAction={pendingFields?.syncTaxes}> - <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> - <Switch - accessibilityLabel={translate('workspace.qbo.taxes')} - isOn={isSwitchOn} - onToggle={() => - Connections.updatePolicyConnectionConfig( - policyID, - CONST.POLICY.CONNECTIONS.NAME.QBO, - CONST.QUICKBOOKS_IMPORTS.SYNC_TAXES, - isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, - ) - } - /> - </View> - </OfflineWithFeedback> + <HeaderWithBackButton title={translate('workspace.qbo.taxes')} /> + <ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}> + <Text style={styles.pb5}>{translate('workspace.qbo.taxesDescription')}</Text> + <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}> + <View style={styles.flex1}> + <Text fontSize={variables.fontSizeNormal}>{translate('workspace.qbo.import')}</Text> </View> - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <OfflineWithFeedback pendingAction={pendingFields?.syncTaxes}> + <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}> + <Switch + accessibilityLabel={translate('workspace.qbo.taxes')} + isOn={isSwitchOn} + onToggle={() => + Connections.updatePolicyConnectionConfig( + policyID, + CONST.POLICY.CONNECTIONS.NAME.QBO, + CONST.QUICKBOOKS_IMPORTS.SYNC_TAXES, + isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, + ) + } + /> + </View> + </OfflineWithFeedback> + </View> + </ScrollView> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 00ce700ecd36..6bde0a156704 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -20,7 +20,6 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -70,61 +69,60 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro ]; return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={CategorySettingsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={CategorySettingsPage.displayName} - > - <HeaderWithBackButton - shouldShowThreeDotsButton - title={route.params.categoryName} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} - threeDotsMenuItems={threeDotsMenuItems} - /> - <ConfirmModal - isVisible={deleteCategoryConfirmModalVisible} - onConfirm={deleteCategory} - onCancel={() => setDeleteCategoryConfirmModalVisible(false)} - title={translate('workspace.categories.deleteCategory')} - prompt={translate('workspace.categories.deleteCategoryPrompt')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorMessageField(policyCategory)} - pendingAction={policyCategory?.pendingFields?.enabled} - errorRowStyles={styles.mh5} - onClose={() => Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)} - > - <View style={[styles.mt2, styles.mh5]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text style={[styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.enableCategory')}</Text> - <Switch - isOn={policyCategory.enabled} - accessibilityLabel={translate('workspace.categories.enableCategory')} - onToggle={updateWorkspaceRequiresCategory} - /> - </View> + <HeaderWithBackButton + shouldShowThreeDotsButton + title={route.params.categoryName} + threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} + threeDotsMenuItems={threeDotsMenuItems} + /> + <ConfirmModal + isVisible={deleteCategoryConfirmModalVisible} + onConfirm={deleteCategory} + onCancel={() => setDeleteCategoryConfirmModalVisible(false)} + title={translate('workspace.categories.deleteCategory')} + prompt={translate('workspace.categories.deleteCategoryPrompt')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorMessageField(policyCategory)} + pendingAction={policyCategory?.pendingFields?.enabled} + errorRowStyles={styles.mh5} + onClose={() => Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)} + > + <View style={[styles.mt2, styles.mh5]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text style={[styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.enableCategory')}</Text> + <Switch + isOn={policyCategory.enabled} + accessibilityLabel={translate('workspace.categories.enableCategory')} + onToggle={updateWorkspaceRequiresCategory} + /> </View> - </OfflineWithFeedback> - <OfflineWithFeedback pendingAction={policyCategory.pendingFields?.name}> - <MenuItemWithTopDescription - title={policyCategory.name} - description={translate(`workspace.categories.categoryName`)} - onPress={navigateToEditCategory} - shouldShowRightIcon - /> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + <OfflineWithFeedback pendingAction={policyCategory.pendingFields?.name}> + <MenuItemWithTopDescription + title={policyCategory.name} + description={translate(`workspace.categories.categoryName`)} + onPress={navigateToEditCategory} + shouldShowRightIcon + /> + </OfflineWithFeedback> + </View> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index 391021e7329d..994bdbe88f93 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -10,7 +10,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -37,27 +36,26 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) ); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={CreateCategoryPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={CreateCategoryPage.displayName} - shouldEnableMaxHeight - > - <HeaderWithBackButton - title={translate('workspace.categories.addCategory')} - onBackButtonPress={Navigation.goBack} - /> - <CategoryForm - onSubmit={createCategory} - policyCategories={policyCategories} - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <HeaderWithBackButton + title={translate('workspace.categories.addCategory')} + onBackButtonPress={Navigation.goBack} + /> + <CategoryForm + onSubmit={createCategory} + policyCategories={policyCategories} + /> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx index 27e226f878c2..5ae11da1f541 100644 --- a/src/pages/workspace/categories/EditCategoryPage.tsx +++ b/src/pages/workspace/categories/EditCategoryPage.tsx @@ -10,7 +10,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -59,29 +58,28 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) { ); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={EditCategoryPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={EditCategoryPage.displayName} - shouldEnableMaxHeight - > - <HeaderWithBackButton - title={translate('workspace.categories.editCategory')} - onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))} - /> - <CategoryForm - onSubmit={editCategory} - validateEdit={validate} - categoryName={currentCategoryName} - policyCategories={policyCategories} - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <HeaderWithBackButton + title={translate('workspace.categories.editCategory')} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))} + /> + <CategoryForm + onSubmit={editCategory} + validateEdit={validate} + categoryName={currentCategoryName} + policyCategories={policyCategories} + /> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index c7accdd79ed6..ebb371c81fba 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -30,7 +30,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -247,70 +246,69 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat const shouldShowEmptyState = !categoryList.some((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) && !isLoading; return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceCategoriesPage.displayName} + shouldShowOfflineIndicatorInWideScreen + offlineIndicatorStyle={styles.mtAuto} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceCategoriesPage.displayName} - shouldShowOfflineIndicatorInWideScreen - offlineIndicatorStyle={styles.mtAuto} + <HeaderWithBackButton + icon={Illustrations.FolderOpen} + title={translate('workspace.common.categories')} + shouldShowBackButton={isSmallScreenWidth} > - <HeaderWithBackButton - icon={Illustrations.FolderOpen} - title={translate('workspace.common.categories')} - shouldShowBackButton={isSmallScreenWidth} - > - {!isSmallScreenWidth && getHeaderButtons()} - </HeaderWithBackButton> - <ConfirmModal - isVisible={deleteCategoriesConfirmModalVisible} - onConfirm={handleDeleteCategories} - onCancel={() => setDeleteCategoriesConfirmModalVisible(false)} - title={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategory' : 'workspace.categories.deleteCategories')} - prompt={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategoryPrompt' : 'workspace.categories.deleteCategoriesPrompt')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + {!isSmallScreenWidth && getHeaderButtons()} + </HeaderWithBackButton> + <ConfirmModal + isVisible={deleteCategoriesConfirmModalVisible} + onConfirm={handleDeleteCategories} + onCancel={() => setDeleteCategoriesConfirmModalVisible(false)} + title={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategory' : 'workspace.categories.deleteCategories')} + prompt={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategoryPrompt' : 'workspace.categories.deleteCategoriesPrompt')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} + <View style={[styles.ph5, styles.pb5, styles.pt3]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.categories.subtitle')}</Text> + </View> + {isLoading && ( + <ActivityIndicator + size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} + style={[styles.flex1]} + color={theme.spinner} + /> + )} + {shouldShowEmptyState && ( + <WorkspaceEmptyStateSection + title={translate('workspace.categories.emptyCategories.title')} + icon={Illustrations.EmptyStateExpenses} + subtitle={translate('workspace.categories.emptyCategories.subtitle')} /> - {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} - <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.categories.subtitle')}</Text> - </View> - {isLoading && ( - <ActivityIndicator - size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} - style={[styles.flex1]} - color={theme.spinner} - /> - )} - {shouldShowEmptyState && ( - <WorkspaceEmptyStateSection - title={translate('workspace.categories.emptyCategories.title')} - icon={Illustrations.EmptyStateExpenses} - subtitle={translate('workspace.categories.emptyCategories.subtitle')} - /> - )} - {!shouldShowEmptyState && !isLoading && ( - <SelectionList - canSelectMultiple - sections={[{data: categoryList, isDisabled: false}]} - onCheckboxPress={toggleCategory} - onSelectRow={navigateToCategorySettings} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - onSelectAll={toggleAllCategories} - showScrollIndicator - ListItem={TableListItem} - onDismissError={dismissError} - customListHeader={getCustomListHeader()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - /> - )} - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + )} + {!shouldShowEmptyState && !isLoading && ( + <SelectionList + canSelectMultiple + sections={[{data: categoryList, isDisabled: false}]} + onCheckboxPress={toggleCategory} + onSelectRow={navigateToCategorySettings} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + onSelectAll={toggleAllCategories} + showScrollIndicator + ListItem={TableListItem} + onDismissError={dismissError} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + /> + )} + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 2a5af39a5337..c377b8c3505d 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -14,7 +14,6 @@ import {setWorkspaceRequiresCategory} from '@libs/actions/Policy'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -37,40 +36,39 @@ function WorkspaceCategoriesSettingsPage({route, policyCategories}: WorkspaceCat const hasEnabledOptions = OptionsListUtils.hasEnabledOptions(policyCategories ?? {}); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} - > - {({policy}) => ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceCategoriesSettingsPage.displayName} - > - <HeaderWithBackButton title={translate('common.settings')} /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={policy?.errorFields?.requiresCategory} - pendingAction={policy?.pendingFields?.requiresCategory} - errorRowStyles={styles.mh5} - > - <View style={[styles.mt2, styles.mh4]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text style={[styles.textNormal, styles.colorMuted, styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.requiresCategory')}</Text> - <Switch - isOn={policy?.requiresCategory ?? false} - accessibilityLabel={translate('workspace.categories.requiresCategory')} - onToggle={updateWorkspaceRequiresCategory} - disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} - /> - </View> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} + > + {({policy}) => ( + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceCategoriesSettingsPage.displayName} + > + <HeaderWithBackButton title={translate('common.settings')} /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={policy?.errorFields?.requiresCategory} + pendingAction={policy?.pendingFields?.requiresCategory} + errorRowStyles={styles.mh5} + > + <View style={[styles.mt2, styles.mh4]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text style={[styles.textNormal, styles.colorMuted, styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.requiresCategory')}</Text> + <Switch + isOn={policy?.requiresCategory ?? false} + accessibilityLabel={translate('workspace.categories.requiresCategory')} + onToggle={updateWorkspaceRequiresCategory} + disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} + /> </View> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - )} - </FeatureEnabledAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + </View> + </ScreenWrapper> + )} </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx index 228a64d4a2ee..3e16d49284fa 100644 --- a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx +++ b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx @@ -15,7 +15,6 @@ import {getOptimisticRateName, validateRateValue} from '@libs/PolicyDistanceRate import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import {createPolicyDistanceRate, generateCustomUnitID} from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -59,41 +58,40 @@ function CreateDistanceRatePage({policy, route}: CreateDistanceRatePageProps) { }; return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={CreateDistanceRatePage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={CreateDistanceRatePage.displayName} - shouldEnableMaxHeight + <HeaderWithBackButton title={translate('workspace.distanceRates.addRate')} /> + <FormProvider + formID={ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM} + submitButtonText={translate('common.save')} + onSubmit={submit} + validate={validate} + enabledWhenOffline + style={[styles.flexGrow1]} + shouldHideFixErrorsAlert + submitFlexEnabled={false} + submitButtonStyles={[styles.mh5, styles.mt0]} + disablePressOnEnter={false} > - <HeaderWithBackButton title={translate('workspace.distanceRates.addRate')} /> - <FormProvider - formID={ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM} - submitButtonText={translate('common.save')} - onSubmit={submit} - validate={validate} - enabledWhenOffline - style={[styles.flexGrow1]} - shouldHideFixErrorsAlert - submitFlexEnabled={false} - submitButtonStyles={[styles.mh5, styles.mt0]} - disablePressOnEnter={false} - > - <InputWrapperWithRef - InputComponent={AmountForm} - inputID={INPUT_IDS.RATE} - extraDecimals={1} - isCurrencyPressable={false} - currency={currency} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <InputWrapperWithRef + InputComponent={AmountForm} + inputID={INPUT_IDS.RATE} + extraDecimals={1} + isCurrencyPressable={false} + currency={currency} + ref={inputCallbackRef} + /> + </FormProvider> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx index 7e215054b1b6..3e2558385266 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx @@ -19,7 +19,6 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -92,74 +91,73 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail }; return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + testID={PolicyDistanceRateDetailsPage.displayName} + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - testID={PolicyDistanceRateDetailsPage.displayName} - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - shouldShowOfflineIndicatorInWideScreen - > - <HeaderWithBackButton - title={`${rateValueToDisplay} / ${translate(`common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`)}`} - shouldShowThreeDotsButton - threeDotsMenuItems={threeDotsMenuItems} - threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} - /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(rate, 'enabled')} - pendingAction={rate?.pendingFields?.enabled} - errorRowStyles={styles.mh5} - onClose={() => clearErrorFields('enabled')} - > - <View style={[styles.flexRow, styles.justifyContentBetween, styles.p5]}> - <Text>{translate('workspace.distanceRates.enableRate')}</Text> - <Switch - isOn={rate.enabled ?? false} - onToggle={toggleRate} - accessibilityLabel={translate('workspace.distanceRates.enableRate')} - /> - </View> - </OfflineWithFeedback> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(rate, 'rate')} - pendingAction={rate?.pendingFields?.rate ?? rate?.pendingFields?.currency} - errorRowStyles={styles.mh5} - onClose={() => clearErrorFields('rate')} - > - <MenuItemWithTopDescription - shouldShowRightIcon - title={`${rateValueToDisplay} / ${unitToDisplay}`} - description={translate('workspace.distanceRates.rate')} - descriptionTextStyle={styles.textNormal} - onPress={editRateValue} + <HeaderWithBackButton + title={`${rateValueToDisplay} / ${translate(`common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`)}`} + shouldShowThreeDotsButton + threeDotsMenuItems={threeDotsMenuItems} + threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} + /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(rate, 'enabled')} + pendingAction={rate?.pendingFields?.enabled} + errorRowStyles={styles.mh5} + onClose={() => clearErrorFields('enabled')} + > + <View style={[styles.flexRow, styles.justifyContentBetween, styles.p5]}> + <Text>{translate('workspace.distanceRates.enableRate')}</Text> + <Switch + isOn={rate.enabled ?? false} + onToggle={toggleRate} + accessibilityLabel={translate('workspace.distanceRates.enableRate')} /> - </OfflineWithFeedback> - <ConfirmModal - onConfirm={() => setIsWarningModalVisible(false)} - isVisible={isWarningModalVisible} - title={translate('workspace.distanceRates.oopsNotSoFast')} - prompt={translate('workspace.distanceRates.workspaceNeeds')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} - /> - <ConfirmModal - title={translate('workspace.distanceRates.deleteDistanceRate')} - isVisible={isDeleteModalVisible} - onConfirm={deleteRate} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={translate('workspace.distanceRates.areYouSureDelete', {count: 1})} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + </View> + </OfflineWithFeedback> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(rate, 'rate')} + pendingAction={rate?.pendingFields?.rate ?? rate?.pendingFields?.currency} + errorRowStyles={styles.mh5} + onClose={() => clearErrorFields('rate')} + > + <MenuItemWithTopDescription + shouldShowRightIcon + title={`${rateValueToDisplay} / ${unitToDisplay}`} + description={translate('workspace.distanceRates.rate')} + descriptionTextStyle={styles.textNormal} + onPress={editRateValue} /> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </OfflineWithFeedback> + <ConfirmModal + onConfirm={() => setIsWarningModalVisible(false)} + isVisible={isWarningModalVisible} + title={translate('workspace.distanceRates.oopsNotSoFast')} + prompt={translate('workspace.distanceRates.workspaceNeeds')} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} + /> + <ConfirmModal + title={translate('workspace.distanceRates.deleteDistanceRate')} + isVisible={isDeleteModalVisible} + onConfirm={deleteRate} + onCancel={() => setIsDeleteModalVisible(false)} + prompt={translate('workspace.distanceRates.areYouSureDelete', {count: 1})} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </View> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx index 805a0b4b111b..9517c94dfaa4 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx @@ -16,7 +16,6 @@ import Navigation from '@libs/Navigation/Navigation'; import {validateRateValue} from '@libs/PolicyDistanceRatesUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -56,46 +55,45 @@ function PolicyDistanceRateEditPage({policy, route}: PolicyDistanceRateEditPageP ); return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={PolicyDistanceRateEditPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={PolicyDistanceRateEditPage.displayName} - shouldEnableMaxHeight + <HeaderWithBackButton + title={translate('workspace.distanceRates.rate')} + shouldShowBackButton + onBackButtonPress={() => Navigation.goBack()} + /> + <FormProvider + formID={ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM} + submitButtonText={translate('common.save')} + onSubmit={submitRate} + validate={validate} + enabledWhenOffline + style={[styles.flexGrow1]} + shouldHideFixErrorsAlert + submitFlexEnabled={false} + submitButtonStyles={[styles.mh5, styles.mt0]} + disablePressOnEnter={false} > - <HeaderWithBackButton - title={translate('workspace.distanceRates.rate')} - shouldShowBackButton - onBackButtonPress={() => Navigation.goBack()} + <InputWrapperWithRef + InputComponent={AmountForm} + inputID={INPUT_IDS.RATE} + extraDecimals={1} + defaultValue={(parseFloat(currentRateValue) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET).toFixed(3)} + isCurrencyPressable={false} + currency={currency} + ref={inputCallbackRef} /> - <FormProvider - formID={ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM} - submitButtonText={translate('common.save')} - onSubmit={submitRate} - validate={validate} - enabledWhenOffline - style={[styles.flexGrow1]} - shouldHideFixErrorsAlert - submitFlexEnabled={false} - submitButtonStyles={[styles.mh5, styles.mt0]} - disablePressOnEnter={false} - > - <InputWrapperWithRef - InputComponent={AmountForm} - inputID={INPUT_IDS.RATE} - extraDecimals={1} - defaultValue={(parseFloat(currentRateValue) / CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET).toFixed(3)} - isCurrencyPressable={false} - currency={currency} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </FormProvider> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 768bfdadcb8a..0127de46feef 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -26,7 +26,6 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu'; import CONST from '@src/CONST'; @@ -265,70 +264,69 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) ); return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={PolicyDistanceRatesPage.displayName} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={PolicyDistanceRatesPage.displayName} - shouldShowOfflineIndicatorInWideScreen + <HeaderWithBackButton + icon={Illustrations.CarIce} + title={translate('workspace.common.distanceRates')} + shouldShowBackButton={isSmallScreenWidth} > - <HeaderWithBackButton - icon={Illustrations.CarIce} - title={translate('workspace.common.distanceRates')} - shouldShowBackButton={isSmallScreenWidth} - > - {!isSmallScreenWidth && headerButtons} - </HeaderWithBackButton> - {isSmallScreenWidth && <View style={[styles.ph5]}>{headerButtons}</View>} - <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.distanceRates.centrallyManage')}</Text> - </View> - {isLoading && ( - <ActivityIndicator - size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} - style={[styles.flex1]} - color={theme.spinner} - /> - )} - {Object.values(customUnitRates).length > 0 && ( - <SelectionList - canSelectMultiple - sections={[{data: distanceRatesList, isDisabled: false}]} - onCheckboxPress={toggleRate} - onSelectRow={openRateDetails} - onSelectAll={toggleAllRates} - onDismissError={dismissError} - showScrollIndicator - ListItem={TableListItem} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - customListHeader={getCustomListHeader()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - /> - )} - <ConfirmModal - onConfirm={() => setIsWarningModalVisible(false)} - isVisible={isWarningModalVisible} - title={translate('workspace.distanceRates.oopsNotSoFast')} - prompt={translate('workspace.distanceRates.workspaceNeeds')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} + {!isSmallScreenWidth && headerButtons} + </HeaderWithBackButton> + {isSmallScreenWidth && <View style={[styles.ph5]}>{headerButtons}</View>} + <View style={[styles.ph5, styles.pb5, styles.pt3]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.distanceRates.centrallyManage')}</Text> + </View> + {isLoading && ( + <ActivityIndicator + size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} + style={[styles.flex1]} + color={theme.spinner} /> - <ConfirmModal - title={translate('workspace.distanceRates.deleteDistanceRate')} - isVisible={isDeleteModalVisible} - onConfirm={deleteRates} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={translate('workspace.distanceRates.areYouSureDelete', {count: selectedDistanceRates.length})} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + )} + {Object.values(customUnitRates).length > 0 && ( + <SelectionList + canSelectMultiple + sections={[{data: distanceRatesList, isDisabled: false}]} + onCheckboxPress={toggleRate} + onSelectRow={openRateDetails} + onSelectAll={toggleAllRates} + onDismissError={dismissError} + showScrollIndicator + ListItem={TableListItem} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + )} + <ConfirmModal + onConfirm={() => setIsWarningModalVisible(false)} + isVisible={isWarningModalVisible} + title={translate('workspace.distanceRates.oopsNotSoFast')} + prompt={translate('workspace.distanceRates.workspaceNeeds')} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} + /> + <ConfirmModal + title={translate('workspace.distanceRates.deleteDistanceRate')} + isVisible={isDeleteModalVisible} + onConfirm={deleteRates} + onCancel={() => setIsDeleteModalVisible(false)} + prompt={translate('workspace.distanceRates.areYouSureDelete', {count: selectedDistanceRates.length})} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index d99b15bb857e..4af3d96da8a7 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -14,7 +14,6 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -67,50 +66,49 @@ function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: Poli }; return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={PolicyDistanceRatesSettingsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={PolicyDistanceRatesSettingsPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.common.settings')} /> - <View style={styles.flexGrow1}> + <HeaderWithBackButton title={translate('workspace.common.settings')} /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(customUnits[customUnitID], 'attributes')} + pendingAction={customUnits[customUnitID].pendingFields?.attributes} + errorRowStyles={styles.mh5} + onClose={() => clearErrorFields('attributes')} + > + <UnitSelector + label={translate('workspace.distanceRates.unit')} + defaultValue={defaultUnit} + wrapperStyle={[styles.ph5, styles.mt3]} + setNewUnit={setNewUnit} + /> + </OfflineWithFeedback> + {policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && ( <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(customUnits[customUnitID], 'attributes')} - pendingAction={customUnits[customUnitID].pendingFields?.attributes} + errors={ErrorUtils.getLatestErrorField(customUnits[customUnitID], 'defaultCategory')} + pendingAction={customUnits[customUnitID].pendingFields?.defaultCategory} errorRowStyles={styles.mh5} - onClose={() => clearErrorFields('attributes')} + onClose={() => clearErrorFields('defaultCategory')} > - <UnitSelector - label={translate('workspace.distanceRates.unit')} - defaultValue={defaultUnit} + <CategorySelector + policyID={policyID} + label={translate('workspace.distanceRates.defaultCategory')} + defaultValue={defaultCategory} wrapperStyle={[styles.ph5, styles.mt3]} - setNewUnit={setNewUnit} + setNewCategory={setNewCategory} /> </OfflineWithFeedback> - {policy?.areCategoriesEnabled && OptionsListUtils.hasEnabledOptions(policyCategories ?? {}) && ( - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(customUnits[customUnitID], 'defaultCategory')} - pendingAction={customUnits[customUnitID].pendingFields?.defaultCategory} - errorRowStyles={styles.mh5} - onClose={() => clearErrorFields('defaultCategory')} - > - <CategorySelector - policyID={policyID} - label={translate('workspace.distanceRates.defaultCategory')} - defaultValue={defaultCategory} - wrapperStyle={[styles.ph5, styles.mt3]} - setNewCategory={setNewCategory} - /> - </OfflineWithFeedback> - )} - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + )} + </View> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index d9151e87553e..7c16a705d89f 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -128,7 +128,10 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM }, [accountID, policyID]); return ( - <AccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + > <ScreenWrapper testID={WorkspaceMemberDetailsPage.displayName}> <HeaderWithBackButton title={displayName} diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx index 55870d9f4d7a..0bae645069c5 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx @@ -35,7 +35,10 @@ function WorkspaceOwnerChangeErrorPage({route}: WorkspaceOwnerChangeSuccessPageP }, [accountID, policyID]); return ( - <AccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + > <ScreenWrapper testID={WorkspaceOwnerChangeErrorPage.displayName}> <HeaderWithBackButton title={translate('workspace.changeOwner.changeOwnerPageTitle')} diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx index 6218ef1208ad..7a5ee8a971b7 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx @@ -29,7 +29,10 @@ function WorkspaceOwnerChangeSuccessPage({route}: WorkspaceOwnerChangeSuccessPag }, [accountID, policyID]); return ( - <AccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + > <ScreenWrapper testID={WorkspaceOwnerChangeSuccessPage.displayName}> <HeaderWithBackButton title={translate('workspace.changeOwner.changeOwnerPageTitle')} diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx index 75106a6a9fe1..a77ac71f4993 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx @@ -54,7 +54,10 @@ function WorkspaceOwnerChangeWrapperPage({route, policy}: WorkspaceOwnerChangeWr }, [accountID, policy, policy?.errorFields?.changeOwner, policyID]); return ( - <AccessOrNotFoundWrapper policyID={policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + > <ScreenWrapper testID={WorkspaceOwnerChangeWrapperPage.displayName}> <HeaderWithBackButton title={translate('workspace.changeOwner.changeOwnerPageTitle')} diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index 04656dc44193..a3945a8a5131 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -17,7 +17,6 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -68,42 +67,41 @@ function EditTagPage({route, policyTags}: EditTagPageProps) { ); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={EditTagPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={EditTagPage.displayName} - shouldEnableMaxHeight + <HeaderWithBackButton + title={translate('workspace.tags.editTag')} + onBackButtonPress={Navigation.goBack} + /> + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAG_FORM} + onSubmit={editTag} + submitButtonText={translate('common.save')} + validate={validate} + style={[styles.mh5, styles.flex1]} + enabledWhenOffline > - <HeaderWithBackButton - title={translate('workspace.tags.editTag')} - onBackButtonPress={Navigation.goBack} + <InputWrapper + InputComponent={TextInput} + maxLength={CONST.TAG_NAME_LIMIT} + defaultValue={currentTagName} + label={translate('common.name')} + accessibilityLabel={translate('common.name')} + inputID={INPUT_IDS.TAG_NAME} + role={CONST.ROLE.PRESENTATION} + ref={inputCallbackRef} /> - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAG_FORM} - onSubmit={editTag} - submitButtonText={translate('common.save')} - validate={validate} - style={[styles.mh5, styles.flex1]} - enabledWhenOffline - > - <InputWrapper - InputComponent={TextInput} - maxLength={CONST.TAG_NAME_LIMIT} - defaultValue={currentTagName} - label={translate('common.name')} - accessibilityLabel={translate('common.name')} - inputID={INPUT_IDS.TAG_NAME} - role={CONST.ROLE.PRESENTATION} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </FormProvider> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index 827487666fb4..f915722f0d1b 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -21,7 +21,6 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -66,69 +65,68 @@ function TagSettingsPage({route, policyTags}: TagSettingsPageProps) { }; return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={TagSettingsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={TagSettingsPage.displayName} - > - <HeaderWithBackButton - title={PolicyUtils.getCleanedTagName(route.params.tagName)} - shouldShowThreeDotsButton - shouldSetModalVisibility={false} - threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} - threeDotsMenuItems={[ - { - icon: Trashcan, - text: translate('workspace.tags.deleteTag'), - onSelected: () => setIsDeleteTagModalOpen(true), - }, - ]} - /> - <ConfirmModal - title={translate('workspace.tags.deleteTag')} - isVisible={isDeleteTagModalOpen} - onConfirm={deleteTagAndHideModal} - onCancel={() => setIsDeleteTagModalOpen(false)} - shouldSetModalVisibility={false} - prompt={translate('workspace.tags.deleteTagConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorMessageField(currentPolicyTag)} - pendingAction={currentPolicyTag.pendingFields?.enabled} - errorRowStyles={styles.mh5} - onClose={() => Policy.clearPolicyTagErrors(route.params.policyID, route.params.tagName)} - > - <View style={[styles.mt2, styles.mh5]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text>{translate('workspace.tags.enableTag')}</Text> - <Switch - isOn={currentPolicyTag.enabled} - accessibilityLabel={translate('workspace.tags.enableTag')} - onToggle={updateWorkspaceTagEnabled} - /> - </View> + <HeaderWithBackButton + title={PolicyUtils.getCleanedTagName(route.params.tagName)} + shouldShowThreeDotsButton + shouldSetModalVisibility={false} + threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} + threeDotsMenuItems={[ + { + icon: Trashcan, + text: translate('workspace.tags.deleteTag'), + onSelected: () => setIsDeleteTagModalOpen(true), + }, + ]} + /> + <ConfirmModal + title={translate('workspace.tags.deleteTag')} + isVisible={isDeleteTagModalOpen} + onConfirm={deleteTagAndHideModal} + onCancel={() => setIsDeleteTagModalOpen(false)} + shouldSetModalVisibility={false} + prompt={translate('workspace.tags.deleteTagConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorMessageField(currentPolicyTag)} + pendingAction={currentPolicyTag.pendingFields?.enabled} + errorRowStyles={styles.mh5} + onClose={() => Policy.clearPolicyTagErrors(route.params.policyID, route.params.tagName)} + > + <View style={[styles.mt2, styles.mh5]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text>{translate('workspace.tags.enableTag')}</Text> + <Switch + isOn={currentPolicyTag.enabled} + accessibilityLabel={translate('workspace.tags.enableTag')} + onToggle={updateWorkspaceTagEnabled} + /> </View> - </OfflineWithFeedback> - <OfflineWithFeedback pendingAction={currentPolicyTag.pendingFields?.name}> - <MenuItemWithTopDescription - title={PolicyUtils.getCleanedTagName(currentPolicyTag.name)} - description={translate(`workspace.tags.tagName`)} - onPress={navigateToEditTag} - shouldShowRightIcon - /> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + <OfflineWithFeedback pendingAction={currentPolicyTag.pendingFields?.name}> + <MenuItemWithTopDescription + title={PolicyUtils.getCleanedTagName(currentPolicyTag.name)} + description={translate(`workspace.tags.tagName`)} + onPress={navigateToEditTag} + shouldShowRightIcon + /> + </OfflineWithFeedback> + </View> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx index 1c4718d93d82..ffc8f3e2c33e 100644 --- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx @@ -18,7 +18,6 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -68,41 +67,40 @@ function CreateTagPage({route, policyTags}: CreateTagPageProps) { ); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={CreateTagPage.displayName} + shouldEnableMaxHeight > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={CreateTagPage.displayName} - shouldEnableMaxHeight + <HeaderWithBackButton + title={translate('workspace.tags.addTag')} + onBackButtonPress={Navigation.goBack} + /> + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAG_FORM} + onSubmit={createTag} + submitButtonText={translate('common.save')} + validate={validate} + style={[styles.mh5, styles.flex1]} + enabledWhenOffline > - <HeaderWithBackButton - title={translate('workspace.tags.addTag')} - onBackButtonPress={Navigation.goBack} + <InputWrapper + InputComponent={TextInput} + maxLength={CONST.TAG_NAME_LIMIT} + label={translate('common.name')} + accessibilityLabel={translate('common.name')} + inputID={INPUT_IDS.TAG_NAME} + role={CONST.ROLE.PRESENTATION} + ref={inputCallbackRef} /> - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAG_FORM} - onSubmit={createTag} - submitButtonText={translate('common.save')} - validate={validate} - style={[styles.mh5, styles.flex1]} - enabledWhenOffline - > - <InputWrapper - InputComponent={TextInput} - maxLength={CONST.TAG_NAME_LIMIT} - label={translate('common.name')} - accessibilityLabel={translate('common.name')} - inputID={INPUT_IDS.TAG_NAME} - role={CONST.ROLE.PRESENTATION} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </FormProvider> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index 202d8bacbbd1..ee50e3bd4c95 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -17,7 +17,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -56,39 +55,38 @@ function WorkspaceEditTagsPage({route, policyTags}: WorkspaceEditTagsPageProps) ); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceEditTagsPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceEditTagsPage.displayName} + <HeaderWithBackButton title={translate(`workspace.tags.customTagName`)} /> + <FormProvider + style={[styles.flexGrow1, styles.ph5]} + formID={ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM} + onSubmit={updateTaglistName} + validate={validateTagName} + submitButtonText={translate('common.save')} + enabledWhenOffline > - <HeaderWithBackButton title={translate(`workspace.tags.customTagName`)} /> - <FormProvider - style={[styles.flexGrow1, styles.ph5]} - formID={ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM} - onSubmit={updateTaglistName} - validate={validateTagName} - submitButtonText={translate('common.save')} - enabledWhenOffline - > - <View style={styles.mb4}> - <InputWrapper - InputComponent={TextInput} - inputID={INPUT_IDS.POLICY_TAGS_NAME} - label={translate(`workspace.tags.customTagName`)} - accessibilityLabel={translate(`workspace.tags.customTagName`)} - defaultValue={PolicyUtils.getCleanedTagName(taglistName)} - role={CONST.ROLE.PRESENTATION} - ref={inputCallbackRef} - /> - </View> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <View style={styles.mb4}> + <InputWrapper + InputComponent={TextInput} + inputID={INPUT_IDS.POLICY_TAGS_NAME} + label={translate(`workspace.tags.customTagName`)} + accessibilityLabel={translate(`workspace.tags.customTagName`)} + defaultValue={PolicyUtils.getCleanedTagName(taglistName)} + role={CONST.ROLE.PRESENTATION} + ref={inputCallbackRef} + /> + </View> + </FormProvider> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 66ed33ad8237..db892131c792 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -256,70 +256,69 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { }; return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceTagsPage.displayName} + shouldShowOfflineIndicatorInWideScreen + offlineIndicatorStyle={styles.mtAuto} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceTagsPage.displayName} - shouldShowOfflineIndicatorInWideScreen - offlineIndicatorStyle={styles.mtAuto} + <HeaderWithBackButton + icon={Illustrations.Tag} + title={translate('workspace.common.tags')} + shouldShowBackButton={isSmallScreenWidth} > - <HeaderWithBackButton - icon={Illustrations.Tag} - title={translate('workspace.common.tags')} - shouldShowBackButton={isSmallScreenWidth} - > - {!isSmallScreenWidth && getHeaderButtons()} - </HeaderWithBackButton> - <ConfirmModal - isVisible={deleteTagsConfirmModalVisible} - onConfirm={handleDeleteTags} - onCancel={() => setDeleteTagsConfirmModalVisible(false)} - title={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags')} - prompt={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTagConfirmation' : 'workspace.tags.deleteTagsConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + {!isSmallScreenWidth && getHeaderButtons()} + </HeaderWithBackButton> + <ConfirmModal + isVisible={deleteTagsConfirmModalVisible} + onConfirm={handleDeleteTags} + onCancel={() => setDeleteTagsConfirmModalVisible(false)} + title={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags')} + prompt={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTagConfirmation' : 'workspace.tags.deleteTagsConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} + <View style={[styles.ph5, styles.pb5, styles.pt3]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.subtitle')}</Text> + </View> + {isLoading && ( + <ActivityIndicator + size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} + style={[styles.flex1]} + color={theme.spinner} + /> + )} + {tagList.length === 0 && !isLoading && ( + <WorkspaceEmptyStateSection + title={translate('workspace.tags.emptyTags.title')} + icon={Illustrations.EmptyStateExpenses} + subtitle={translate('workspace.tags.emptyTags.subtitle')} /> - {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} - <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.subtitle')}</Text> - </View> - {isLoading && ( - <ActivityIndicator - size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} - style={[styles.flex1]} - color={theme.spinner} - /> - )} - {tagList.length === 0 && !isLoading && ( - <WorkspaceEmptyStateSection - title={translate('workspace.tags.emptyTags.title')} - icon={Illustrations.EmptyStateExpenses} - subtitle={translate('workspace.tags.emptyTags.subtitle')} - /> - )} - {tagList.length > 0 && !isLoading && ( - <SelectionList - canSelectMultiple - sections={[{data: tagList, isDisabled: false}]} - onCheckboxPress={toggleTag} - onSelectRow={navigateToTagSettings} - onSelectAll={toggleAllTags} - showScrollIndicator - ListItem={TableListItem} - customListHeader={getCustomListHeader()} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - onDismissError={(item) => Policy.clearPolicyTagErrors(route.params.policyID, item.value)} - /> - )} - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + )} + {tagList.length > 0 && !isLoading && ( + <SelectionList + canSelectMultiple + sections={[{data: tagList, isDisabled: false}]} + onCheckboxPress={toggleTag} + onSelectRow={navigateToTagSettings} + onSelectAll={toggleAllTags} + showScrollIndicator + ListItem={TableListItem} + customListHeader={getCustomListHeader()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + onDismissError={(item) => Policy.clearPolicyTagErrors(route.params.policyID, item.value)} + /> + )} + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index e82e11588ebb..8c26d80b8ed5 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -16,7 +16,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -41,51 +40,50 @@ function WorkspaceTagsSettingsPage({route, policyTags}: WorkspaceTagsSettingsPag [route.params.policyID], ); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} - > - {({policy}) => ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceTagsSettingsPage.displayName} - > - <HeaderWithBackButton title={translate('common.settings')} /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={policy?.errorFields?.requiresTag} - pendingAction={policy?.pendingFields?.requiresTag} - errorRowStyles={styles.mh5} - > - <View style={[styles.mt2, styles.mh4]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.requiresTag')}</Text> - <Switch - isOn={policy?.requiresTag ?? false} - accessibilityLabel={translate('workspace.tags.requiresTag')} - onToggle={updateWorkspaceRequiresTag} - /> - </View> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} + > + {({policy}) => ( + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceTagsSettingsPage.displayName} + > + <HeaderWithBackButton title={translate('common.settings')} /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={policy?.errorFields?.requiresTag} + pendingAction={policy?.pendingFields?.requiresTag} + errorRowStyles={styles.mh5} + > + <View style={[styles.mt2, styles.mh4]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.requiresTag')}</Text> + <Switch + isOn={policy?.requiresTag ?? false} + accessibilityLabel={translate('workspace.tags.requiresTag')} + onToggle={updateWorkspaceRequiresTag} + /> </View> - </OfflineWithFeedback> - <OfflineWithFeedback - errors={policyTags?.[policyTagName]?.errors} - pendingAction={policyTags?.[policyTagName]?.pendingAction} - errorRowStyles={styles.mh5} - > - <MenuItemWithTopDescription - title={policyTagName} - description={translate(`workspace.tags.customTagName`)} - onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))} - shouldShowRightIcon - /> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - )} - </FeatureEnabledAccessOrNotFoundWrapper> + </View> + </OfflineWithFeedback> + <OfflineWithFeedback + errors={policyTags?.[policyTagName]?.errors} + pendingAction={policyTags?.[policyTagName]?.pendingAction} + errorRowStyles={styles.mh5} + > + <MenuItemWithTopDescription + title={policyTagName} + description={translate(`workspace.tags.customTagName`)} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))} + shouldShowRightIcon + /> + </OfflineWithFeedback> + </View> + </ScreenWrapper> + )} </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/NamePage.tsx b/src/pages/workspace/taxes/NamePage.tsx index ee52b7273c8f..f42d6f7c9a21 100644 --- a/src/pages/workspace/taxes/NamePage.tsx +++ b/src/pages/workspace/taxes/NamePage.tsx @@ -17,7 +17,6 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -68,45 +67,44 @@ function NamePage({ } return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={NamePage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={NamePage.displayName} - > - <HeaderWithBackButton - title={translate('common.name')} - onBackButtonPress={goBack} - /> + <HeaderWithBackButton + title={translate('common.name')} + onBackButtonPress={goBack} + /> - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM} - submitButtonText={translate('workspace.editor.save')} - style={[styles.flexGrow1, styles.ph5]} - onSubmit={submit} - enabledWhenOffline - validate={validate} - > - <View style={styles.mb4}> - <InputWrapper - InputComponent={TextInput} - role={CONST.ROLE.PRESENTATION} - inputID={INPUT_IDS.NAME} - label={translate('workspace.editor.nameInputLabel')} - accessibilityLabel={translate('workspace.editor.nameInputLabel')} - value={name} - maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} - onChangeText={setName} - ref={inputCallbackRef} - /> - </View> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM} + submitButtonText={translate('workspace.editor.save')} + style={[styles.flexGrow1, styles.ph5]} + onSubmit={submit} + enabledWhenOffline + validate={validate} + > + <View style={styles.mb4}> + <InputWrapper + InputComponent={TextInput} + role={CONST.ROLE.PRESENTATION} + inputID={INPUT_IDS.NAME} + label={translate('workspace.editor.nameInputLabel')} + accessibilityLabel={translate('workspace.editor.nameInputLabel')} + value={name} + maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} + onChangeText={setName} + ref={inputCallbackRef} + /> + </View> + </FormProvider> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx index 90b9eda860c4..c241ee8fba1b 100644 --- a/src/pages/workspace/taxes/ValuePage.tsx +++ b/src/pages/workspace/taxes/ValuePage.tsx @@ -16,7 +16,6 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -54,49 +53,48 @@ function ValuePage({ } return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={ValuePage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={ValuePage.displayName} + <HeaderWithBackButton + title={translate('workspace.taxes.value')} + onBackButtonPress={goBack} + /> + + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM} + submitButtonText={translate('workspace.editor.save')} + style={[styles.flexGrow1]} + scrollContextEnabled + validate={validateTaxValue} + onSubmit={submit} + enabledWhenOffline + disablePressOnEnter={false} + shouldHideFixErrorsAlert + submitFlexEnabled={false} + submitButtonStyles={[styles.mh5, styles.mt0]} > - <HeaderWithBackButton - title={translate('workspace.taxes.value')} - onBackButtonPress={goBack} + <InputWrapper + InputComponent={AmountForm} + inputID={INPUT_IDS.VALUE} + defaultValue={defaultValue} + hideCurrencySymbol + // The default currency uses 2 decimal places, so we substract it + extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2} + // We increase the amount max length to support the extra decimals. + amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES} + extraSymbol={<Text style={styles.iouAmountText}>%</Text>} + ref={inputCallbackRef} /> - - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM} - submitButtonText={translate('workspace.editor.save')} - style={[styles.flexGrow1]} - scrollContextEnabled - validate={validateTaxValue} - onSubmit={submit} - enabledWhenOffline - disablePressOnEnter={false} - shouldHideFixErrorsAlert - submitFlexEnabled={false} - submitButtonStyles={[styles.mh5, styles.mt0]} - > - <InputWrapper - InputComponent={AmountForm} - inputID={INPUT_IDS.VALUE} - defaultValue={defaultValue} - hideCurrencySymbol - // The default currency uses 2 decimal places, so we substract it - extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2} - // We increase the amount max length to support the extra decimals. - amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES} - extraSymbol={<Text style={styles.iouAmountText}>%</Text>} - ref={inputCallbackRef} - /> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </FormProvider> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx index 3b75664fd23a..121d32e1662b 100644 --- a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx @@ -15,7 +15,6 @@ import {createPolicyTax, getNextTaxCode, getTaxValueWithPercentage, validateTaxN import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -62,59 +61,58 @@ function WorkspaceCreateTaxPage({ ); return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + testID={WorkspaceCreateTaxPage.displayName} + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} > - <ScreenWrapper - testID={WorkspaceCreateTaxPage.displayName} - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - > - <View style={[styles.h100, styles.flex1, styles.justifyContentBetween]}> - <HeaderWithBackButton title={translate('workspace.taxes.addRate')} /> - <FormProvider - style={[styles.flexGrow1, styles.mh5]} - formID={ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM} - onSubmit={submitForm} - validate={validateForm} - submitButtonText={translate('common.save')} - enabledWhenOffline - shouldValidateOnBlur={false} - disablePressOnEnter={false} - > - <View style={styles.mhn5}> - <InputWrapper - InputComponent={TextPicker} - inputID={INPUT_IDS.NAME} - label={translate('common.name')} - description={translate('common.name')} - rightLabel={translate('common.required')} - accessibilityLabel={translate('workspace.editor.nameInputLabel')} - maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} - multiline={false} - role={CONST.ROLE.PRESENTATION} - autoFocus - /> - <InputWrapper - InputComponent={AmountPicker} - inputID={INPUT_IDS.VALUE} - title={(v) => (v ? getTaxValueWithPercentage(v) : '')} - description={translate('workspace.taxes.value')} - rightLabel={translate('common.required')} - hideCurrencySymbol - // The default currency uses 2 decimal places, so we substract it - extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2} - // We increase the amount max length to support the extra decimals. - amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES} - extraSymbol={<Text style={styles.iouAmountText}>%</Text>} - /> - </View> - </FormProvider> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <View style={[styles.h100, styles.flex1, styles.justifyContentBetween]}> + <HeaderWithBackButton title={translate('workspace.taxes.addRate')} /> + <FormProvider + style={[styles.flexGrow1, styles.mh5]} + formID={ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM} + onSubmit={submitForm} + validate={validateForm} + submitButtonText={translate('common.save')} + enabledWhenOffline + shouldValidateOnBlur={false} + disablePressOnEnter={false} + > + <View style={styles.mhn5}> + <InputWrapper + InputComponent={TextPicker} + inputID={INPUT_IDS.NAME} + label={translate('common.name')} + description={translate('common.name')} + rightLabel={translate('common.required')} + accessibilityLabel={translate('workspace.editor.nameInputLabel')} + maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} + multiline={false} + role={CONST.ROLE.PRESENTATION} + autoFocus + /> + <InputWrapper + InputComponent={AmountPicker} + inputID={INPUT_IDS.VALUE} + title={(v) => (v ? getTaxValueWithPercentage(v) : '')} + description={translate('workspace.taxes.value')} + rightLabel={translate('common.required')} + hideCurrencySymbol + // The default currency uses 2 decimal places, so we substract it + extraDecimals={CONST.MAX_TAX_RATE_DECIMAL_PLACES - 2} + // We increase the amount max length to support the extra decimals. + amountMaxLength={CONST.MAX_TAX_RATE_DECIMAL_PLACES + CONST.MAX_TAX_RATE_INTEGER_PLACES} + extraSymbol={<Text style={styles.iouAmountText}>%</Text>} + /> + </View> + </FormProvider> + </View> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 649ccf7579a9..d4f46fbc2dd6 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -20,7 +20,6 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -74,83 +73,82 @@ function WorkspaceEditTaxPage({ } return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + testID={WorkspaceEditTaxPage.displayName} + style={styles.mb5} > - <ScreenWrapper - testID={WorkspaceEditTaxPage.displayName} - style={styles.mb5} - > - <View style={[styles.h100, styles.flex1]}> - <HeaderWithBackButton + <View style={[styles.h100, styles.flex1]}> + <HeaderWithBackButton + title={currentTaxRate?.name} + threeDotsMenuItems={threeDotsMenuItems} + shouldShowThreeDotsButton={!!canEdit} + threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} + /> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'isDisabled')} + pendingAction={currentTaxRate?.pendingFields?.isDisabled} + errorRowStyles={styles.mh5} + onClose={() => clearTaxRateFieldError(policyID, taxID, 'isDisabled')} + > + <View style={[styles.mt2, styles.mh5]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text>{translate('workspace.taxes.actions.enable')}</Text> + <Switch + isOn={!currentTaxRate?.isDisabled} + accessibilityLabel={translate('workspace.taxes.actions.enable')} + onToggle={toggleTaxRate} + disabled={!canEdit} + /> + </View> + </View> + </OfflineWithFeedback> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'name')} + pendingAction={currentTaxRate?.pendingFields?.name} + errorRowStyles={styles.mh5} + onClose={() => clearTaxRateFieldError(policyID, taxID, 'name')} + > + <MenuItemWithTopDescription + shouldShowRightIcon title={currentTaxRate?.name} - threeDotsMenuItems={threeDotsMenuItems} - shouldShowThreeDotsButton={!!canEdit} - threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} + description={translate('common.name')} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAX_NAME.getRoute(`${policyID}`, taxID))} /> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'isDisabled')} - pendingAction={currentTaxRate?.pendingFields?.isDisabled} - errorRowStyles={styles.mh5} - onClose={() => clearTaxRateFieldError(policyID, taxID, 'isDisabled')} - > - <View style={[styles.mt2, styles.mh5]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text>{translate('workspace.taxes.actions.enable')}</Text> - <Switch - isOn={!currentTaxRate?.isDisabled} - accessibilityLabel={translate('workspace.taxes.actions.enable')} - onToggle={toggleTaxRate} - disabled={!canEdit} - /> - </View> - </View> - </OfflineWithFeedback> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'name')} - pendingAction={currentTaxRate?.pendingFields?.name} - errorRowStyles={styles.mh5} - onClose={() => clearTaxRateFieldError(policyID, taxID, 'name')} - > - <MenuItemWithTopDescription - shouldShowRightIcon - title={currentTaxRate?.name} - description={translate('common.name')} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAX_NAME.getRoute(`${policyID}`, taxID))} - /> - </OfflineWithFeedback> - <OfflineWithFeedback - errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'value')} - pendingAction={currentTaxRate?.pendingFields?.value} - errorRowStyles={styles.mh5} - onClose={() => clearTaxRateFieldError(policyID, taxID, 'value')} - > - <MenuItemWithTopDescription - shouldShowRightIcon - title={currentTaxRate?.value} - description={translate('workspace.taxes.value')} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAX_VALUE.getRoute(`${policyID}`, taxID))} - /> - </OfflineWithFeedback> - </View> - <ConfirmModal - title={translate('workspace.taxes.actions.delete')} - isVisible={isDeleteModalVisible} - onConfirm={deleteTaxRate} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={translate('workspace.taxes.deleteTaxConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </OfflineWithFeedback> + <OfflineWithFeedback + errors={ErrorUtils.getLatestErrorField(currentTaxRate, 'value')} + pendingAction={currentTaxRate?.pendingFields?.value} + errorRowStyles={styles.mh5} + onClose={() => clearTaxRateFieldError(policyID, taxID, 'value')} + > + <MenuItemWithTopDescription + shouldShowRightIcon + title={currentTaxRate?.value} + description={translate('workspace.taxes.value')} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_TAX_VALUE.getRoute(`${policyID}`, taxID))} + /> + </OfflineWithFeedback> + </View> + <ConfirmModal + title={translate('workspace.taxes.actions.delete')} + isVisible={isDeleteModalVisible} + onConfirm={deleteTaxRate} + onCancel={() => setIsDeleteModalVisible(false)} + prompt={translate('workspace.taxes.deleteTaxConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index afcb7ac08792..d1b6ad50bf3d 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -28,7 +28,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -237,66 +236,65 @@ function WorkspaceTaxesPage({ ); return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceTaxesPage.displayName} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceTaxesPage.displayName} - shouldShowOfflineIndicatorInWideScreen + <HeaderWithBackButton + icon={Illustrations.Coins} + title={translate('workspace.common.taxes')} + shouldShowBackButton={isSmallScreenWidth} > - <HeaderWithBackButton - icon={Illustrations.Coins} - title={translate('workspace.common.taxes')} - shouldShowBackButton={isSmallScreenWidth} - > - {!isSmallScreenWidth && headerButtons} - </HeaderWithBackButton> + {!isSmallScreenWidth && headerButtons} + </HeaderWithBackButton> - {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{headerButtons}</View>} + {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{headerButtons}</View>} - <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.taxes.subtitle')}</Text> - </View> - {isLoading && ( - <ActivityIndicator - size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} - style={[styles.flex1]} - color={theme.spinner} - /> - )} - <SelectionList - canSelectMultiple - sections={[{data: taxesList, isDisabled: false}]} - onCheckboxPress={toggleTax} - onSelectRow={navigateToEditTaxRate} - onSelectAll={toggleAllTaxes} - showScrollIndicator - ListItem={TableListItem} - customListHeader={getCustomListHeader()} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - onDismissError={(item) => (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)} + <View style={[styles.ph5, styles.pb5, styles.pt3]}> + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.taxes.subtitle')}</Text> + </View> + {isLoading && ( + <ActivityIndicator + size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} + style={[styles.flex1]} + color={theme.spinner} /> - <ConfirmModal - title={translate('workspace.taxes.actions.delete')} - isVisible={isDeleteModalVisible} - onConfirm={deleteTaxes} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={ - selectedTaxesIDs.length > 1 - ? translate('workspace.taxes.deleteMultipleTaxConfirmation', {taxAmount: selectedTaxesIDs.length}) - : translate('workspace.taxes.deleteTaxConfirmation') - } - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + )} + <SelectionList + canSelectMultiple + sections={[{data: taxesList, isDisabled: false}]} + onCheckboxPress={toggleTax} + onSelectRow={navigateToEditTaxRate} + onSelectAll={toggleAllTaxes} + showScrollIndicator + ListItem={TableListItem} + customListHeader={getCustomListHeader()} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + onDismissError={(item) => (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)} + /> + <ConfirmModal + title={translate('workspace.taxes.actions.delete')} + isVisible={isDeleteModalVisible} + onConfirm={deleteTaxes} + onCancel={() => setIsDeleteModalVisible(false)} + prompt={ + selectedTaxesIDs.length > 1 + ? translate('workspace.taxes.deleteMultipleTaxConfirmation', {taxAmount: selectedTaxesIDs.length}) + : translate('workspace.taxes.deleteTaxConfirmation') + } + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx index 073cd1b7adab..d5cb1c9d31e9 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx @@ -15,7 +15,6 @@ import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as ValidationUtils from '@libs/ValidationUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -54,44 +53,43 @@ function WorkspaceTaxesSettingsCustomTaxName({ }; return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceTaxesSettingsCustomTaxName.displayName} + style={styles.defaultModalContainer} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceTaxesSettingsCustomTaxName.displayName} - style={styles.defaultModalContainer} - > - <HeaderWithBackButton title={translate('workspace.taxes.customTaxName')} /> + <HeaderWithBackButton title={translate('workspace.taxes.customTaxName')} /> - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME} - submitButtonText={translate('workspace.editor.save')} - style={[styles.flexGrow1, styles.ph5]} - scrollContextEnabled - enabledWhenOffline - validate={validate} - onSubmit={submit} - > - <View style={styles.mb4}> - <InputWrapper - InputComponent={TextInput} - role={CONST.ROLE.PRESENTATION} - inputID={INPUT_IDS.NAME} - label={translate('workspace.editor.nameInputLabel')} - accessibilityLabel={translate('workspace.editor.nameInputLabel')} - defaultValue={policy?.taxRates?.name} - maxLength={CONST.TAX_RATES.CUSTOM_NAME_MAX_LENGTH} - multiline={false} - ref={inputCallbackRef} - /> - </View> - </FormProvider> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME} + submitButtonText={translate('workspace.editor.save')} + style={[styles.flexGrow1, styles.ph5]} + scrollContextEnabled + enabledWhenOffline + validate={validate} + onSubmit={submit} + > + <View style={styles.mb4}> + <InputWrapper + InputComponent={TextInput} + role={CONST.ROLE.PRESENTATION} + inputID={INPUT_IDS.NAME} + label={translate('workspace.editor.nameInputLabel')} + accessibilityLabel={translate('workspace.editor.nameInputLabel')} + defaultValue={policy?.taxRates?.name} + maxLength={CONST.TAX_RATES.CUSTOM_NAME_MAX_LENGTH} + multiline={false} + ref={inputCallbackRef} + /> + </View> + </FormProvider> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx index d7aba1fa36a8..ebc756da5622 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx @@ -12,7 +12,6 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import type * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -43,33 +42,32 @@ function WorkspaceTaxesSettingsForeignCurrency({ }; return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceTaxesSettingsForeignCurrency.displayName} + style={styles.defaultModalContainer} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceTaxesSettingsForeignCurrency.displayName} - style={styles.defaultModalContainer} - > - {({insets}) => ( - <> - <HeaderWithBackButton title={translate('workspace.taxes.foreignDefault')} /> + {({insets}) => ( + <> + <HeaderWithBackButton title={translate('workspace.taxes.foreignDefault')} /> - <View style={[styles.mb4, styles.flex1]}> - <TaxPicker - selectedTaxRate={selectedTaxRate} - policyID={policyID} - insets={insets} - onSubmit={submit} - /> - </View> - </> - )} - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <View style={[styles.mb4, styles.flex1]}> + <TaxPicker + selectedTaxRate={selectedTaxRate} + policyID={policyID} + insets={insets} + onSubmit={submit} + /> + </View> + </> + )} + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx index 3ee015b6ad77..bb0f48f2daae 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx @@ -11,7 +11,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -54,37 +53,36 @@ function WorkspaceTaxesSettingsPage({ ); return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + testID={WorkspaceTaxesSettingsPage.displayName} + style={styles.defaultModalContainer} > - <ScreenWrapper - testID={WorkspaceTaxesSettingsPage.displayName} - style={styles.defaultModalContainer} - > - <ScrollView contentContainerStyle={styles.flexGrow1}> - <HeaderWithBackButton title={translate('common.settings')} /> - <View style={styles.flex1}> - {menuItems.map((item) => ( - <OfflineWithFeedback - key={item.description} - pendingAction={item.pendingAction} - > - <MenuItemWithTopDescription - shouldShowRightIcon - title={item.title} - description={item.description} - style={[styles.moneyRequestMenuItem]} - titleStyle={styles.flex1} - onPress={item.action} - /> - </OfflineWithFeedback> - ))} - </View> - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <ScrollView contentContainerStyle={styles.flexGrow1}> + <HeaderWithBackButton title={translate('common.settings')} /> + <View style={styles.flex1}> + {menuItems.map((item) => ( + <OfflineWithFeedback + key={item.description} + pendingAction={item.pendingAction} + > + <MenuItemWithTopDescription + shouldShowRightIcon + title={item.title} + description={item.description} + style={[styles.moneyRequestMenuItem]} + titleStyle={styles.flex1} + onPress={item.action} + /> + </OfflineWithFeedback> + ))} + </View> + </ScrollView> + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx index 78f0b9bb5904..1113ed3268a8 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx @@ -12,7 +12,6 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import type * as OptionsListUtils from '@libs/OptionsListUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import CONST from '@src/CONST'; @@ -39,33 +38,32 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({ }; return ( - <AccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={policyID} + featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceTaxesSettingsWorkspaceCurrency.displayName} + style={styles.defaultModalContainer} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceTaxesSettingsWorkspaceCurrency.displayName} - style={styles.defaultModalContainer} - > - {({insets}) => ( - <> - <HeaderWithBackButton title={translate('workspace.taxes.workspaceDefault')} /> + {({insets}) => ( + <> + <HeaderWithBackButton title={translate('workspace.taxes.workspaceDefault')} /> - <View style={[styles.mb4, styles.flex1]}> - <TaxPicker - selectedTaxRate={selectedTaxRate} - policyID={policyID} - insets={insets} - onSubmit={submit} - /> - </View> - </> - )} - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + <View style={[styles.mb4, styles.flex1]}> + <TaxPicker + selectedTaxRate={selectedTaxRate} + policyID={policyID} + insets={insets} + onSubmit={submit} + /> + </View> + </> + )} + </ScreenWrapper> </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx index 5d2297f47ddd..07553baba356 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx @@ -15,7 +15,7 @@ import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import * as Policy from '@userActions/Policy'; @@ -118,7 +118,7 @@ function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoRepor ); return ( - <FeatureEnabledAccessOrNotFoundWrapper + <AccessOrNotFoundWrapper policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED} > @@ -149,7 +149,7 @@ function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoRepor </OfflineWithFeedback> </FullPageNotFoundView> </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx index 0b44d6c565d1..614e17e5b9c9 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx @@ -10,7 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import * as Policy from '@userActions/Policy'; @@ -72,7 +72,7 @@ function WorkspaceAutoReportingMonthlyOffsetPage({policy, route}: WorkspaceAutoR }; return ( - <FeatureEnabledAccessOrNotFoundWrapper + <AccessOrNotFoundWrapper policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED} > @@ -104,7 +104,7 @@ function WorkspaceAutoReportingMonthlyOffsetPage({policy, route}: WorkspaceAutoR /> </FullPageNotFoundView> </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx index d32218616662..1b77838eba24 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx @@ -21,7 +21,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as UserUtils from '@libs/UserUtils'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import * as Policy from '@userActions/Policy'; @@ -165,7 +165,7 @@ function WorkspaceWorkflowsApproverPage({policy, personalDetails, isLoadingRepor }; return ( - <FeatureEnabledAccessOrNotFoundWrapper + <AccessOrNotFoundWrapper policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED} > @@ -196,7 +196,7 @@ function WorkspaceWorkflowsApproverPage({policy, personalDetails, isLoadingRepor /> </FullPageNotFoundView> </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 35412d768bcc..d5753f15c218 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -20,7 +20,7 @@ import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; @@ -264,7 +264,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr const isLoading = Boolean(policy?.isLoading && policy?.reimbursementChoice === undefined); return ( - <FeatureEnabledAccessOrNotFoundWrapper + <AccessOrNotFoundWrapper policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED} > @@ -303,7 +303,7 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr </Section> </View> </WorkspacePageWithSections> - </FeatureEnabledAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx index 58f5d4c54e97..64a0fb88523a 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -175,7 +175,10 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR ); return ( - <AccessOrNotFoundWrapper policyID={route.params.policyID}> + <AccessOrNotFoundWrapper + accessVariants={['ADMIN', 'PAID']} + policyID={route.params.policyID} + > <FullPageNotFoundView shouldShow={shouldShowNotFoundPage} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} From 4852a4e9cd0963b37ac2099ec7f29a42827ff74e Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 19 Apr 2024 22:26:59 +0700 Subject: [PATCH 239/580] Fix workspace avatar in workspace switcher --- src/components/WorkspaceSwitcherButton.tsx | 7 ++++--- src/pages/WorkspaceSwitcherPage/index.tsx | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx index 0e2465b09995..118fcd0f10a9 100644 --- a/src/components/WorkspaceSwitcherButton.tsx +++ b/src/components/WorkspaceSwitcherButton.tsx @@ -26,7 +26,7 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { const pressableRef = useRef<HTMLDivElement | View | null>(null); - const {source, name, type} = useMemo(() => { + const {source, name, type, id} = useMemo(() => { if (!policy) { return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR}; } @@ -34,8 +34,9 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { const avatar = policy?.avatar ? policy.avatar : getDefaultWorkspaceAvatar(policy?.name); return { source: avatar, - name: policy?.id ?? '', + name: policy?.name ?? '', type: CONST.ICON_TYPE_WORKSPACE, + id: policy?.id ?? '', }; }, [policy]); @@ -55,7 +56,7 @@ function WorkspaceSwitcherButton({policy}: WorkspaceSwitcherButtonProps) { > {({hovered}) => ( <SubscriptAvatar - mainAvatar={{source, name, type}} + mainAvatar={{source, name, type, id}} subscriptIcon={{ source: Expensicons.DownArrow, width: CONST.WORKSPACE_SWITCHER.SUBSCRIPT_ICON_SIZE, diff --git a/src/pages/WorkspaceSwitcherPage/index.tsx b/src/pages/WorkspaceSwitcherPage/index.tsx index 0a5492536d56..f58439ea0110 100644 --- a/src/pages/WorkspaceSwitcherPage/index.tsx +++ b/src/pages/WorkspaceSwitcherPage/index.tsx @@ -118,6 +118,7 @@ function WorkspaceSwitcherPage() { fallbackIcon: Expensicons.FallbackWorkspaceAvatar, name: policy?.name, type: CONST.ICON_TYPE_WORKSPACE, + id: policy?.id, }, ], isBold: hasUnreadData(policy?.id), From 7bec8ab39972df9c759fcf5e75237c0beb45d237 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Fri, 19 Apr 2024 23:48:41 +0800 Subject: [PATCH 240/580] update jsdoc --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 4bcdf82c0b9c..22f9afbc16b4 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -223,8 +223,8 @@ function ReimbursementAccountPage({ /** * Retrieve verified business bank account currently being set up. - * @param ignoreLocalSubStep Pass true if you want the last "updated" view (from db), not the last "viewed" view (from onyx). * @param ignoreLocalCurrentStep Pass true if you want the last "updated" view (from db), not the last "viewed" view (from onyx). + * @param ignoreLocalSubStep Pass true if you want the last "updated" view (from db), not the last "viewed" view (from onyx). */ function fetchData(ignoreLocalCurrentStep?: boolean, ignoreLocalSubStep?: boolean) { // Show loader right away, as optimisticData might be set only later in case multiple calls are in the queue From 70d02e43feaf8519f36b49d80d2ff3c496d99727 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Fri, 19 Apr 2024 19:46:04 +0200 Subject: [PATCH 241/580] fix nameValuePairs bugs --- src/libs/CardUtils.ts | 2 +- src/libs/migrations/RenameCardIsVirtual.ts | 4 ++-- src/pages/settings/Wallet/ExpensifyCardPage.tsx | 10 +++++----- src/pages/settings/Wallet/PaymentMethodList.tsx | 7 ++++--- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 9e3a7f66131a..106debd0a7e5 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -127,7 +127,7 @@ function maskCard(lastFour = ''): string { * @returns a physical card object (or undefined if none is found) */ function findPhysicalCard(cards: Card[]) { - return cards.find((card) => !card.nameValuePairs?.isVirtual); + return cards.find((card) => !card?.nameValuePairs?.isVirtual); } /** diff --git a/src/libs/migrations/RenameCardIsVirtual.ts b/src/libs/migrations/RenameCardIsVirtual.ts index 4d03a9687a70..5929b39dfe72 100644 --- a/src/libs/migrations/RenameCardIsVirtual.ts +++ b/src/libs/migrations/RenameCardIsVirtual.ts @@ -19,7 +19,7 @@ export default function () { Log.info('[Migrate Onyx] Skipped migration RenameCardIsVirtual because there are no cards linked to the account'); return resolve(); } - const cardsWithIsVirtualProp = Object.values(cardList).filter((card) => card.isVirtual !== undefined); + const cardsWithIsVirtualProp = Object.values(cardList).filter((card) => card?.nameValuePairs?.isVirtual !== undefined); if (!cardsWithIsVirtualProp.length) { Log.info('[Migrate Onyx] Skipped migration RenameCardIsVirtual because there were no cards with the isVirtual property'); return resolve(); @@ -34,7 +34,7 @@ export default function () { ...result, [card.cardID]: { nameValuePairs: { - isVirtual: card.isVirtual, + isVirtual: card?.nameValuePairs?.isVirtual, }, isVirtual: undefined, }, diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index e0b5e45464d4..af92d917754e 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -84,13 +84,13 @@ function ExpensifyCardPage({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const shouldDisplayCardDomain = !cardList?.[cardID].nameValuePairs?.issuedBy; - const pageTitle = shouldDisplayCardDomain ? translate('cardPage.expensifyCard') : cardList?.[cardID].nameValuePairs?.cardTitle ?? translate('cardPage.expensifyCard'); + const shouldDisplayCardDomain = !cardList?.[cardID]?.nameValuePairs?.issuedBy; + const pageTitle = shouldDisplayCardDomain ? translate('cardPage.expensifyCard') : cardList?.[cardID]?.nameValuePairs?.cardTitle ?? translate('cardPage.expensifyCard'); const [isNotFound, setIsNotFound] = useState(false); const cardsToShow = useMemo(() => { if (shouldDisplayCardDomain) { - return CardUtils.getDomainCards(cardList)[domain].filter((card) => !card.nameValuePairs?.issuedBy); + return CardUtils.getDomainCards(cardList)[domain]?.filter((card) => !card?.nameValuePairs?.issuedBy); } return [cardList?.[cardID]]; }, [shouldDisplayCardDomain, cardList, cardID, domain]); @@ -98,8 +98,8 @@ function ExpensifyCardPage({ setIsNotFound(!cardsToShow); }, [cardList, cardsToShow]); - const virtualCards = useMemo(() => cardsToShow.filter((card) => card.isVirtual), [cardsToShow]); - const physicalCards = useMemo(() => cardsToShow.filter((card) => !card.isVirtual), [cardsToShow]); + const virtualCards = useMemo(() => cardsToShow.filter((card) => card?.nameValuePairs?.isVirtual), [cardsToShow]); + const physicalCards = useMemo(() => cardsToShow.filter((card) => !card?.nameValuePairs?.isVirtual), [cardsToShow]); const [cardsDetails, setCardsDetails] = useState<Record<number, TCardDetails | null>>({}); const [isCardDetailsLoading, setIsCardDetailsLoading] = useState<Record<number, boolean>>({}); const [cardsDetailsErrors, setCardsDetailsErrors] = useState<Record<number, string>>({}); diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index f883907f6e2c..3edaa8282b56 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -222,7 +222,7 @@ function PaymentMethodList({ return; } - const isAdminIssuedVirtualCard = !!card.nameValuePairs?.issuedBy; + const isAdminIssuedVirtualCard = !!card?.nameValuePairs?.issuedBy; // The card should be grouped to a specific domain and such domain already exists in a assignedCardsGrouped if (assignedCardsGrouped.some((item) => item.isGroupedCardDomain && item.description === card.domainName) && !isAdminIssuedVirtualCard) { @@ -238,7 +238,7 @@ function PaymentMethodList({ assignedCardsGrouped.push({ key: card.cardID.toString(), // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - title: isAdminIssuedVirtualCard ? card.nameValuePairs?.cardTitle || card.bank : card.bank, + title: isAdminIssuedVirtualCard ? card?.nameValuePairs?.cardTitle || card.bank : card.bank, description: card.domainName, onPress: () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName ?? '', card.cardID.toString() ?? '')), isGroupedCardDomain: !isAdminIssuedVirtualCard, @@ -253,6 +253,7 @@ function PaymentMethodList({ ...icon, }); }); + console.log('%%%%%\n', 'assignedCardsGrouped', assignedCardsGrouped); return assignedCardsGrouped; } @@ -297,7 +298,7 @@ function PaymentMethodList({ shouldShowRightIcon: true, }; }); - + console.log('%%%%%\n', 'combinedPaymentMethods', combinedPaymentMethods); return combinedPaymentMethods; }, [shouldShowAssignedCards, fundList, bankAccountList, styles, filterType, isOffline, cardList, actionPaymentMethodType, activePaymentMethodID, StyleUtils, onPress]); From 887f41025e33a9277afd17f3ef6062f60c8d2ee0 Mon Sep 17 00:00:00 2001 From: rory <rory@expensify.com> Date: Fri, 19 Apr 2024 12:35:22 -0700 Subject: [PATCH 242/580] Remove unused import --- src/pages/home/report/FloatingMessageCounter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/FloatingMessageCounter.tsx b/src/pages/home/report/FloatingMessageCounter.tsx index 8e73d36a878a..0d92946e3d66 100644 --- a/src/pages/home/report/FloatingMessageCounter.tsx +++ b/src/pages/home/report/FloatingMessageCounter.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withSpring} from 'react-native-reanimated'; import Button from '@components/Button'; From 23738607a65c0f418b2c1c78c2398e95d0bf070a Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 20 Apr 2024 01:23:14 +0530 Subject: [PATCH 243/580] Update --- .eslintrc.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 0cecabdc9cdd..809576f3de76 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -287,7 +287,7 @@ module.exports = { files: ['en.ts', 'es.ts'], rules: { 'rulesdir/use-periods-for-error-messages': 'error', - } - } + }, + }, ], }; From 980248746b0babba61e298f4a883402cf2e250af Mon Sep 17 00:00:00 2001 From: GandalfGwaihir <whogandalf@gmail.com> Date: Sat, 20 Apr 2024 02:34:07 +0530 Subject: [PATCH 244/580] fix merge conflict --- src/components/MoneyRequestHeader.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 8b88509fc5f0..b8f2c7cd1dbd 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -96,16 +96,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, if (isOnHold) { IOU.unholdRequest(iouTransactionID, report?.reportID); } else { - if (!policy?.type) { - return; - } - const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); -<<<<<<< HEAD Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, iouTransactionID, report?.reportID, activeRoute)); -======= - Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy.type, iouTransactionID, report?.reportID, activeRoute)); ->>>>>>> main } }; From 0ad104cb29486b6a60ac515b4cbb228cb3197f1f Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Sat, 20 Apr 2024 09:35:35 +0700 Subject: [PATCH 245/580] fix update svg files --- assets/images/{tax.svg => coins.svg} | 0 assets/images/receipt-scan.svg | 14 ++++++++++++++ src/components/Icon/Expensicons.ts | 6 ++++-- src/components/TabSelector/TabSelector.tsx | 2 +- .../getIconForAction/index.ts | 4 ++-- .../FloatingActionButtonAndPopover.tsx | 2 +- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- 7 files changed, 23 insertions(+), 7 deletions(-) rename assets/images/{tax.svg => coins.svg} (100%) create mode 100644 assets/images/receipt-scan.svg diff --git a/assets/images/tax.svg b/assets/images/coins.svg similarity index 100% rename from assets/images/tax.svg rename to assets/images/coins.svg diff --git a/assets/images/receipt-scan.svg b/assets/images/receipt-scan.svg new file mode 100644 index 000000000000..ecdf3cf2e115 --- /dev/null +++ b/assets/images/receipt-scan.svg @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 28.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> +<path d="M3,1.364V7h14V1.364c0-0.372-0.458-0.545-0.701-0.265l-0.748,0.861c-0.159,0.184-0.443,0.184-0.602,0l-1.148-1.322 + c-0.159-0.184-0.443-0.184-0.602,0l-1.148,1.322c-0.159,0.184-0.443,0.184-0.602,0l-1.148-1.322c-0.159-0.184-0.443-0.184-0.602,0 + L8.551,1.959c-0.159,0.184-0.443,0.184-0.602,0L6.801,0.638c-0.159-0.184-0.443-0.184-0.602,0L5.051,1.959 + c-0.159,0.184-0.443,0.184-0.602,0L3.701,1.098C3.458,0.818,3,0.991,3,1.364z"/> +<path d="M3,18.636V13h14v5.636c0,0.372-0.458,0.545-0.701,0.265l-0.748-0.861c-0.159-0.184-0.443-0.184-0.602,0l-1.148,1.322 + c-0.159,0.184-0.443,0.184-0.602,0l-1.148-1.322c-0.159-0.184-0.443-0.184-0.602,0l-1.148,1.322c-0.159,0.184-0.443,0.184-0.602,0 + L8.551,18.04c-0.159-0.184-0.443-0.184-0.602,0l-1.148,1.322c-0.159,0.184-0.443,0.184-0.602,0L5.051,18.04 + c-0.159-0.184-0.443-0.184-0.602,0l-0.748,0.861C3.458,19.182,3,19.008,3,18.636z"/> +<path d="M2,9c-0.552,0-1,0.448-1,1c0,0.552,0.448,1,1,1h16c0.552,0,1-0.448,1-1c0-0.552-0.448-1-1-1H2z"/> +</svg> diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 877e4972a3ec..4807031f5b83 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -37,6 +37,7 @@ import ChatBubbles from '@assets/images/chatbubbles.svg'; import Checkmark from '@assets/images/checkmark.svg'; import Close from '@assets/images/close.svg'; import ClosedSign from '@assets/images/closed-sign.svg'; +import Coins from '@assets/images/coins.svg'; import Collapse from '@assets/images/collapse.svg'; import Concierge from '@assets/images/concierge.svg'; import Connect from '@assets/images/connect.svg'; @@ -122,6 +123,7 @@ import Printer from '@assets/images/printer.svg'; import Profile from '@assets/images/profile.svg'; import QrCode from '@assets/images/qrcode.svg'; import QuestionMark from '@assets/images/question-mark-circle.svg'; +import ReceiptScan from '@assets/images/receipt-scan.svg'; import ReceiptSearch from '@assets/images/receipt-search.svg'; import Receipt from '@assets/images/receipt.svg'; import RemoveMembers from '@assets/images/remove-members.svg'; @@ -142,7 +144,6 @@ import Stopwatch from '@assets/images/stopwatch.svg'; import Sync from '@assets/images/sync.svg'; import Tag from '@assets/images/tag.svg'; import Task from '@assets/images/task.svg'; -import Tax from '@assets/images/tax.svg'; import Thread from '@assets/images/thread.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; @@ -232,7 +233,7 @@ export { Fullscreen, Folder, Tag, - Tax, + Coins, Thread, Gallery, Gear, @@ -286,6 +287,7 @@ export { QrCode, QuestionMark, Receipt, + ReceiptScan, RemoveMembers, ReceiptSearch, Rotate, diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index fc19e6a8062e..9fe1665f6b19 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -26,7 +26,7 @@ function getIconAndTitle(route: string, translate: LocaleContextProps['translate case CONST.TAB_REQUEST.MANUAL: return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')}; case CONST.TAB_REQUEST.SCAN: - return {icon: Expensicons.Receipt, title: translate('tabSelector.scan')}; + return {icon: Expensicons.ReceiptScan, title: translate('tabSelector.scan')}; case CONST.TAB.NEW_CHAT: return {icon: Expensicons.User, title: translate('tabSelector.chat')}; case CONST.TAB.NEW_ROOM: diff --git a/src/libs/focusComposerWithDelay/getIconForAction/index.ts b/src/libs/focusComposerWithDelay/getIconForAction/index.ts index a1b7ee001d5f..963c92640f39 100644 --- a/src/libs/focusComposerWithDelay/getIconForAction/index.ts +++ b/src/libs/focusComposerWithDelay/getIconForAction/index.ts @@ -1,11 +1,11 @@ -import {ValueOf} from 'type-fest'; +import type {ValueOf} from 'type-fest'; import * as Expensicons from '@components/Icon/Expensicons'; import CONST from '@src/CONST'; const getIconForAction = (actionType: ValueOf<typeof CONST.IOU.TYPE>) => { switch (actionType) { case CONST.IOU.TYPE.TRACK_EXPENSE: - return Expensicons.MoneyCircle; + return Expensicons.Coins; case CONST.IOU.TYPE.REQUEST: return Expensicons.Receipt; case CONST.IOU.TYPE.SEND: diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 41acad611562..019ed232ff7b 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -86,7 +86,7 @@ const getQuickActionIcon = (action: QuickActionName): React.FC<SvgProps> => { case CONST.QUICK_ACTIONS.REQUEST_MANUAL: return Expensicons.MoneyCircle; case CONST.QUICK_ACTIONS.REQUEST_SCAN: - return Expensicons.Receipt; + return Expensicons.ReceiptScan; case CONST.QUICK_ACTIONS.REQUEST_DISTANCE: return Expensicons.Car; case CONST.QUICK_ACTIONS.SPLIT_MANUAL: diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index cbdacf66c548..231bbdb92d24 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -208,7 +208,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc if (policy?.tax?.trackingEnabled) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.taxes', - icon: Expensicons.Tax, + icon: Expensicons.Coins, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.TAXES, brickRoadIndicator: PolicyUtils.hasTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, From e8413bb28abda5c98b05164ac78db08c8dabcddd Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Sat, 20 Apr 2024 10:05:52 +0700 Subject: [PATCH 246/580] fix refactor --- .../{focusComposerWithDelay => }/getIconForAction/index.ts | 0 .../ReportActionCompose/AttachmentPickerWithMenuItems.tsx | 2 +- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/libs/{focusComposerWithDelay => }/getIconForAction/index.ts (100%) diff --git a/src/libs/focusComposerWithDelay/getIconForAction/index.ts b/src/libs/getIconForAction/index.ts similarity index 100% rename from src/libs/focusComposerWithDelay/getIconForAction/index.ts rename to src/libs/getIconForAction/index.ts diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 515ce8921c43..e2d87d55605a 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -18,7 +18,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; -import getIconForAction from '@libs/focusComposerWithDelay/getIconForAction'; +import getIconForAction from '@libs/getIconForAction'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 019ed232ff7b..3710733f31b3 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -14,7 +14,7 @@ import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import getIconForAction from '@libs/focusComposerWithDelay/getIconForAction'; +import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import * as ReportUtils from '@libs/ReportUtils'; @@ -94,7 +94,7 @@ const getQuickActionIcon = (action: QuickActionName): React.FC<SvgProps> => { case CONST.QUICK_ACTIONS.SPLIT_DISTANCE: return Expensicons.Transfer; case CONST.QUICK_ACTIONS.SEND_MONEY: - return Expensicons.Send; + return getIconForAction(CONST.IOU.TYPE.SEND); case CONST.QUICK_ACTIONS.ASSIGN_TASK: return Expensicons.Task; case CONST.QUICK_ACTIONS.TRACK_DISTANCE: From c7cd9a9a33bacb1f5672001d992c4049bee68389 Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Sat, 20 Apr 2024 10:11:30 +0700 Subject: [PATCH 247/580] fix send money icon --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index b955f6d3c7ff..fb556ccaad81 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -324,7 +324,7 @@ function FloatingActionButtonAndPopover( ), }, { - icon: getIconForAction(CONST.IOU.TYPE.SEND), + icon: Expensicons.Transfer, text: translate('iou.splitExpense'), onSelected: () => interceptAnonymousUser(() => @@ -337,7 +337,7 @@ function FloatingActionButtonAndPopover( ), }, { - icon: Expensicons.Send, + icon: getIconForAction(CONST.IOU.TYPE.SEND), text: translate('iou.paySomeone', {}), onSelected: () => interceptAnonymousUser(() => From eb29d41a76b9bedbb2ab22a2c99b6d561d8eefed Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Sat, 20 Apr 2024 14:03:28 +0800 Subject: [PATCH 248/580] fix multiple open report when mounted --- src/pages/home/ReportScreen.tsx | 7 ------- src/pages/home/report/ReportActionsView.tsx | 18 ------------------ 2 files changed, 25 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index cbd388d0f7dc..db42b101238e 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -416,13 +416,6 @@ function ReportScreen({ return; } - // It is possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that - // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. - // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === reportIDFromRoute && !reportMetadata?.isLoadingInitialReportActions) { - return; - } - if (!shouldFetchReport(report)) { return; } diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 3e3ebf1a9cc3..6579db9bd76d 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -235,24 +235,6 @@ function ReportActionsView({ const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; - useEffect(() => { - if (reportActionID) { - return; - } - - const interactionTask = InteractionManager.runAfterInteractions(() => { - openReportIfNecessary(); - }); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - if (interactionTask) { - return () => { - interactionTask.cancel(); - }; - } - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useEffect(() => { if (!reportActionID || indexOfLinkedAction > -1) { return; From 257c1de6f9b710d55df435351cd42d1d302fb1d7 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Sat, 20 Apr 2024 14:21:22 +0800 Subject: [PATCH 249/580] remove unnecessary deps --- src/pages/home/ReportScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index db42b101238e..74babb9b0b41 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -421,7 +421,7 @@ function ReportScreen({ } fetchReport(); - }, [report, reportMetadata?.isLoadingInitialReportActions, fetchReport, reportIDFromRoute]); + }, [report, fetchReport, reportIDFromRoute]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); From f5b9eea9ee37b48d1bd2d8fbf6473356fee7f305 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Sat, 20 Apr 2024 09:44:37 -0500 Subject: [PATCH 250/580] Create Automatically-submit-employee-reports.md New article for automatically submitting employee reports --- .../Automatically-submit-employee-reports.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md diff --git a/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md b/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md new file mode 100644 index 000000000000..ffe5f1cf2273 --- /dev/null +++ b/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md @@ -0,0 +1,41 @@ +--- +title: Automatically submit employee reports +description: Use Expensify's Scheduled Submit feature to have your employees' expenses submitted automatically for them +--- +<div id="expensify-classic" markdown="1"> + +Scheduled Submit automatically adds expenses to a report and sends them for approval so that your employees do not have to remember to manually submit their reports each week. This allows you to automatically collect employee expenses on a schedule of your choosing. + +With Scheduled Submit, an employee's expenses are automatically gathered onto a report as soon as they create them. If there is not an existing report, a new one is created. The report is then automatically submitted at the cadence you choose—daily, weekly, monthly, twice per month, or by trip. + +{% include info.html %} +If an expense has a violation, Scheduled Submit will not automatically submit it until the violations are corrected. In the meantime, the expense will be removed from the report and added to an open report. +{% include end-info.html %} + +# Enable Scheduled Submit + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left (or click the Individual tab to enable Scheduled Submit for your individual workspace). +3. Click the desired workspace name. +4. Click the **Reports** tab on the left. +5. Click the Schedule Submit toggle to enable it. +6. Click the “How often expenses submit” dropdown and select the submission schedule: + - **Daily**: Expenses are submitted every evening. Expenses with violations are submitted the day after the violations are corrected. + - **Weekly**: Expenses are submitted once a week. Expenses with violations are submitted the following Sunday after the violations are corrected. + - **Twice a month**: Expenses are submitted on the 15th and the last day of each month. Expenses with violations are submitted at the next cycle (either on the 15th or the last day of the month, whichever is closest). + - **Monthly**: Expenses are submitted once per month. If you select Monthly, you will also select which day of the month the reports will be submitted. Expenses with violations are submitted on the next monthly submission date. + - **By trip**: All expenses that occur in a similar time frame are grouped together. The trip report is created after no new expenses have been submitted for two calendar days. Then the report is submitted the second day, and any new expenses are added to a new trip report. + - **Manually**: Expenses are automatically added to an open report, but the report will require manual submission—it will not be submitted automatically. This is a great option for automatically gathering an employee’s expenses on a report while still requiring the employee to review and submit their report. + +{% include info.html %} +- All submission times are in the evening PDT. +- If you enable Scheduled Submit for your individual workspace and one of your group workspaces also has Scheduled Submit enabled, the group’s submission settings will override your individual workspace settings. +{% include end-info.html %} + +# FAQs + +**I disabled Scheduled Submit. Why do I still get reports submitted by Concierge?** + +Although an Admin can disable scheduled submit for a workspace, employees have the ability to activate schedule submit for their account. If you disable Scheduled Submit but still receive reports from Concierge, the employee has Schedule Submit activated for their individual workspace. + +</div> From 6ff4fc9d895635bb1cb3a3850a9b82825c2a4d37 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:26:51 -0500 Subject: [PATCH 251/580] Create Assign-tag-and-category-approvers.md New article for assigning tag and category approvers --- .../Assign-tag-and-category-approvers.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md diff --git a/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md b/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md new file mode 100644 index 000000000000..a831d770160c --- /dev/null +++ b/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md @@ -0,0 +1,35 @@ +--- +title: Assign tag and category approvers +description: Require an approver for expenses coded with a specific tag or category +--- +<div id="expensify-classic" markdown="1"> + +Once your workplace has created tags and categories, approvers can be assigned to them. Tag and category approvers are automatically added to the report approval workflow when a submitted expense contains a specific tag or category. + +For example, if all employees are required to tag project-based expenses with a tag for the project, you can assign the project manager as the approver for that tag. This way, when a report is submitted containing expenses with that project tag, it will first be routed to the project manager for approval before continuing through the rest of the approval workflow. + +If a report contains multiple categories or tags that each require a different reviewer, then each reviewer must review the report before it can be submitted. The report will first go to the category approvers, the tag approvers, and then the approvers assigned in the approval workflow. + +# Assign category approvers + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Categories** tab on the left. +5. Locate the category in the list of categories and click **Edit**. +6. Click the Approver field to select an approver. +7. Click **Save**. + +# Assign tag approvers + +{% include info.html %} +Tag approvers are only supported for a single level of tags, not for multi-level tags. +{% include end-info.html %} + +1. Hover over Settings, then click **Workspaces**. +2. Click the **Group** tab on the left. +3. Click the desired workspace name. +4. Click the **Tags** tab on the left. +5. Locate the tags list In the list of tags and click the Approver field to assign an approver. + +</div> From 26ded32c930227a9d377e27651a12cad663feeca Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Sun, 21 Apr 2024 20:33:38 +0200 Subject: [PATCH 252/580] minor improvements of OnyxUpdateMangaer --- src/libs/actions/OnyxUpdateManager.ts | 28 +++++++++++++-------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 71b0eda3465f..9d035b84c3e7 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -19,15 +19,15 @@ import * as OnyxUpdates from './OnyxUpdates'; // 6. Restart the sequential queue // 7. Restart the Onyx updates from Pusher // This will ensure that the client is up-to-date with the server and all the updates have been applied in the correct order. -// It's important that this file is separate and not imported by OnyxUpdates.js, so that there are no circular dependencies. Onyx -// is used as a pub/sub mechanism to break out of the circular dependency. -// The circular dependency happens because this file calls API.GetMissingOnyxUpdates() which uses the SaveResponseInOnyx.js file -// (as a middleware). Therefore, SaveResponseInOnyx.js can't import and use this file directly. +// It's important that this file is separate and not imported by OnyxUpdates.js, so that there are no circular dependencies. +// Onyx is used as a pub/sub mechanism to break out of the circular dependency. +// The circular dependency happens because this file calls API.GetMissingOnyxUpdates() which uses the SaveResponseInOnyx.js file (as a middleware). +// Therefore, SaveResponseInOnyx.js can't import and use this file directly. -let lastUpdateIDAppliedToClient: number | null = 0; +let lastUpdateIDAppliedToClient = 0; Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (value) => (lastUpdateIDAppliedToClient = value), + callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), }); let isLoadingApp = false; @@ -104,12 +104,12 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSpl } let updatesAfterGaps: DeferredUpdatesDictionary = {}; - if (gapExists && firstUpdateAfterGaps) { + if (gapExists) { updatesAfterGaps = Object.entries(updates).reduce<DeferredUpdatesDictionary>( (accUpdates, [lastUpdateID, update]) => ({ ...accUpdates, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ...(Number(lastUpdateID) >= firstUpdateAfterGaps! ? {[Number(lastUpdateID)]: update} : {}), + ...(Number(lastUpdateID) >= firstUpdateAfterGaps ? {[Number(lastUpdateID)]: update} : {}), }), {}, ); @@ -126,10 +126,7 @@ function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { const pendingDeferredUpdates = Object.entries(deferredUpdates).reduce<DeferredUpdatesDictionary>( (accUpdates, [lastUpdateID, update]) => ({ ...accUpdates, - // It should not be possible for lastUpdateIDAppliedToClient to be null, - // after the missing updates have been applied. - // If still so we want to keep the deferred update in the list. - ...(!lastUpdateIDAppliedToClient || (Number(lastUpdateID) ?? 0) > lastUpdateIDAppliedToClient ? {[Number(lastUpdateID)]: update} : {}), + ...(Number(lastUpdateID) > lastUpdateIDAppliedToClient ? {[Number(lastUpdateID)]: update} : {}), }), {}, ); @@ -147,6 +144,7 @@ function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { if (latestMissingUpdateID) { return new Promise((resolve, reject) => { deferredUpdates = {}; + applyUpdates(applicableUpdates).then(() => { // After we have applied the applicable updates, there might have been new deferred updates added. // In the next (recursive) call of "validateAndApplyDeferredUpdates", @@ -156,7 +154,7 @@ function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { // It should not be possible for lastUpdateIDAppliedToClient to be null, therefore we can ignore this case. // If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates. - if (!lastUpdateIDAppliedToClient || latestMissingUpdateID <= lastUpdateIDAppliedToClient) { + if (latestMissingUpdateID <= lastUpdateIDAppliedToClient) { validateAndApplyDeferredUpdates().then(resolve).catch(reject); return; } @@ -177,7 +175,7 @@ function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { * @param clientLastUpdateID an optional override for the lastUpdateIDAppliedToClient * @returns */ -function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromServer>, clientLastUpdateID = 0) { +function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromServer>, clientLastUpdateID?: number) { // If isLoadingApp is positive it means that OpenApp command hasn't finished yet, and in that case // we don't have base state of the app (reports, policies, etc) setup. If we apply this update, // we'll only have them overriten by the openApp response. So let's skip it and return. @@ -203,7 +201,7 @@ function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromSer const updateParams = onyxUpdatesFromServer; const lastUpdateIDFromServer = onyxUpdatesFromServer.lastUpdateID; const previousUpdateIDFromServer = onyxUpdatesFromServer.previousUpdateID; - const lastUpdateIDFromClient = clientLastUpdateID || lastUpdateIDAppliedToClient; + const lastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0; // In cases where we received a previousUpdateID and it doesn't match our lastUpdateIDAppliedToClient // we need to perform one of the 2 possible cases: From e21abf5be569a3c0ed6ec7c41df990e8ea580a36 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Sun, 21 Apr 2024 20:41:22 +0200 Subject: [PATCH 253/580] implement tests --- tests/unit/OnyxUpdateManagerTest.ts | 165 ++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 tests/unit/OnyxUpdateManagerTest.ts diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts new file mode 100644 index 000000000000..cab9862dd90d --- /dev/null +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -0,0 +1,165 @@ +import Onyx from 'react-native-onyx'; +import * as App from '@libs/actions/App'; +import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; +import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; + +const lastAppliedMissingUpdateIDKey = 'lastAppliedMissingUpdateID'; + +const createTriggerPromise = () => { + let trigger: () => void = () => undefined; + const resetPromise = () => + new Promise<void>((resolve) => { + trigger = resolve; + }); + const promise = resetPromise(); + + return {promise, trigger, resetPromise}; +}; + +type AppMockType = typeof App & { + getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; + getMissingOnyxUpdatesTriggeredPromise: Promise<void>; +}; + +jest.mock('@libs/actions/App', () => { + const AppImplementation: typeof App = jest.requireActual('@libs/actions/App'); + const AppOnyx: typeof Onyx = jest.requireActual('react-native-onyx').default; + const APP_ONYXKEYS: typeof ONYXKEYS = jest.requireActual('@src/ONYXKEYS').default; + + let appLastAppliedMissingUpdateID = 1; + AppOnyx.connect({ + // @ts-expect-error ignore invalid onyx key + key: 'lastAppliedMissingUpdateID', + callback: (value) => (appLastAppliedMissingUpdateID = (value as number | null) ?? 1), + }); + + const {promise: getMissingOnyxUpdatesTriggeredPromise, trigger: getMissingOnyxUpdatesWasTriggered, resetPromise: resetGetMissingOnyxUpdatesPromise} = createTriggerPromise(); + + return { + ...AppImplementation, + finalReconnectAppAfterActivatingReliableUpdates: jest.fn(() => Promise.resolve()), + getMissingOnyxUpdatesTriggeredPromise, + getMissingOnyxUpdates: jest.fn(() => { + getMissingOnyxUpdatesWasTriggered(); + + const promise = AppOnyx.set(APP_ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, appLastAppliedMissingUpdateID); + + promise.finally(() => { + resetGetMissingOnyxUpdatesPromise(); + }); + + return promise; + }), + } as AppMockType; +}); + +type OnyxUpdateManagerMockType = typeof OnyxUpdateManager & { + applyUpdates: jest.Mock<Promise<Response[]>>; + applyUpdatesTriggeredPromise: Promise<void>; +}; + +jest.mock('@libs/actions/OnyxUpdateManager', () => { + const OnyxUpdateManagerImplementation: typeof OnyxUpdateManager = jest.requireActual('@libs/actions/OnyxUpdateManager'); + + const {promise: applyUpdatesTriggeredPromise, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggeredPromise} = createTriggerPromise(); + + return { + ...OnyxUpdateManagerImplementation, + applyUpdatesTriggeredPromise, + applyUpdates: jest.fn((updates: DeferredUpdatesDictionary) => { + applyUpdatesTriggered(); + + const promise = OnyxUpdateManagerImplementation.applyUpdates(updates); + + promise.finally(() => { + resetApplyUpdatesTriggeredPromise(); + }); + + return promise; + }), + } as OnyxUpdateManagerMockType; +}); + +const AppMock = App as AppMockType; +const OnyxUpdateManagerMock = OnyxUpdateManager as OnyxUpdateManagerMockType; + +let lastUpdateIDAppliedToClient = 0; +Onyx.connect({ + key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, + callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), +}); + +const createMockUpdate = (lastUpdateID: number): OnyxUpdatesFromServer => ({ + type: 'https', + lastUpdateID, + previousUpdateID: lastUpdateID - 1, + request: { + command: 'TestCommand', + successData: [], + failureData: [], + finallyData: [], + optimisticData: [], + }, + response: { + lastUpdateID, + previousUpdateID: lastUpdateID - 1, + }, + updates: [ + { + eventType: 'test', + data: [], + }, + ], +}); +const mockUpdate3 = createMockUpdate(3); +const mockUpdate4 = createMockUpdate(4); +const mockUpdate5 = createMockUpdate(5); +const mockUpdate6 = createMockUpdate(6); + +describe('OnyxUpdateManager', () => { + beforeEach(() => { + Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); + OnyxUpdateManager.resetDeferralLogicVariables(); + }); + + it('should fetch missing Onyx updates once, defer updates and apply after missing updates', async () => { + // @ts-expect-error ignore invalid onyx key + await Onyx.set(lastAppliedMissingUpdateIDKey, 1); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); + + AppMock.getMissingOnyxUpdatesTriggeredPromise.then(() => { + expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); + }); + + OnyxUpdateManager.queryPromise?.then(() => { + expect(lastUpdateIDAppliedToClient).toBe(4); + expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); + }); + }); + + it('should only apply deferred updates that are after the locally applied update', async () => { + // @ts-expect-error ignore invalid onyx key + await Onyx.set(lastAppliedMissingUpdateIDKey, 3); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); + + AppMock.getMissingOnyxUpdatesTriggeredPromise.then(() => { + expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); + Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); + }); + + OnyxUpdateManager.queryPromise?.then(() => { + expect(lastUpdateIDAppliedToClient).toBe(6); + expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); + }); + }); +}); From cbe2d9f19020c705ca388e05e5244281e987e38d Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Sun, 21 Apr 2024 20:41:35 +0200 Subject: [PATCH 254/580] add exports for testing --- src/libs/actions/OnyxUpdateManager.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index 9d035b84c3e7..66bfccde89cb 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -38,16 +38,22 @@ Onyx.connect({ }, }); +// eslint-disable-next-line import/no-mutable-exports let queryPromise: Promise<Response | Response[] | void> | undefined; type DeferredUpdatesDictionary = Record<number, OnyxUpdatesFromServer>; +// eslint-disable-next-line import/no-mutable-exports let deferredUpdates: DeferredUpdatesDictionary = {}; +const resetDeferralLogicVariables = () => { + queryPromise = undefined; + deferredUpdates = {}; +}; + // This function will reset the query variables, unpause the SequentialQueue and log an info to the user. function finalizeUpdatesAndResumeQueue() { console.debug('[OnyxUpdateManager] Done applying all updates'); - queryPromise = undefined; - deferredUpdates = {}; + resetDeferralLogicVariables(); Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); SequentialQueue.unpause(); } @@ -260,3 +266,5 @@ export default () => { }; export {handleOnyxUpdateGap}; +export {queryPromise, deferredUpdates, applyUpdates, resetDeferralLogicVariables}; +export type {DeferredUpdatesDictionary}; From 2a290797b7c93903b796ce006f8b208321fc2a0b Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Sun, 21 Apr 2024 22:36:29 +0200 Subject: [PATCH 255/580] fix: test implementation --- tests/unit/OnyxUpdateManagerTest.ts | 79 ++++++++++++++++------------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index cab9862dd90d..9fcf81850be9 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -1,11 +1,12 @@ import Onyx from 'react-native-onyx'; -import * as App from '@libs/actions/App'; -import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; +import * as AppImport from '@libs/actions/App'; +import * as OnyxUpdateManagerImport from '@libs/actions/OnyxUpdateManager'; import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager'; +// import {deferredUpdates, handleOnyxUpdateGap, queryPromise} from '@libs/actions/OnyxUpdateManager'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; -const lastAppliedMissingUpdateIDKey = 'lastAppliedMissingUpdateID'; +const lastAppliedMissingUpdateIDOnyxKey = 'lastAppliedMissingUpdateID'; const createTriggerPromise = () => { let trigger: () => void = () => undefined; @@ -18,21 +19,21 @@ const createTriggerPromise = () => { return {promise, trigger, resetPromise}; }; -type AppMockType = typeof App & { +type AppActionsMock = typeof AppImport & { getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; getMissingOnyxUpdatesTriggeredPromise: Promise<void>; }; jest.mock('@libs/actions/App', () => { - const AppImplementation: typeof App = jest.requireActual('@libs/actions/App'); + const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); const AppOnyx: typeof Onyx = jest.requireActual('react-native-onyx').default; const APP_ONYXKEYS: typeof ONYXKEYS = jest.requireActual('@src/ONYXKEYS').default; - let appLastAppliedMissingUpdateID = 1; + let appLastAppliedMissingUpdateID = 2; AppOnyx.connect({ // @ts-expect-error ignore invalid onyx key key: 'lastAppliedMissingUpdateID', - callback: (value) => (appLastAppliedMissingUpdateID = (value as number | null) ?? 1), + callback: (value) => (appLastAppliedMissingUpdateID = (value as number | null) ?? 2), }); const {promise: getMissingOnyxUpdatesTriggeredPromise, trigger: getMissingOnyxUpdatesWasTriggered, resetPromise: resetGetMissingOnyxUpdatesPromise} = createTriggerPromise(); @@ -52,38 +53,45 @@ jest.mock('@libs/actions/App', () => { return promise; }), - } as AppMockType; + } as AppActionsMock; }); -type OnyxUpdateManagerMockType = typeof OnyxUpdateManager & { +type OnyxUpdateManagerMock = typeof OnyxUpdateManagerImport & { applyUpdates: jest.Mock<Promise<Response[]>>; applyUpdatesTriggeredPromise: Promise<void>; }; jest.mock('@libs/actions/OnyxUpdateManager', () => { - const OnyxUpdateManagerImplementation: typeof OnyxUpdateManager = jest.requireActual('@libs/actions/OnyxUpdateManager'); + const OnyxUpdateManagerImplementation: typeof OnyxUpdateManagerImport = jest.requireActual('@libs/actions/OnyxUpdateManager'); const {promise: applyUpdatesTriggeredPromise, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggeredPromise} = createTriggerPromise(); - return { - ...OnyxUpdateManagerImplementation, - applyUpdatesTriggeredPromise, - applyUpdates: jest.fn((updates: DeferredUpdatesDictionary) => { - applyUpdatesTriggered(); - - const promise = OnyxUpdateManagerImplementation.applyUpdates(updates); - - promise.finally(() => { - resetApplyUpdatesTriggeredPromise(); - }); - - return promise; - }), - } as OnyxUpdateManagerMockType; + return new Proxy(OnyxUpdateManagerImplementation, { + get: (target, prop) => { + switch (prop) { + case 'applyUpdatesTriggeredPromise': + return applyUpdatesTriggeredPromise; + case 'applyUpdates': + return jest.fn((updates: DeferredUpdatesDictionary) => { + applyUpdatesTriggered(); + + const promise = OnyxUpdateManagerImplementation.applyUpdates(updates); + + promise.finally(() => { + resetApplyUpdatesTriggeredPromise(); + }); + + return promise; + }); + default: + return target[prop as keyof typeof OnyxUpdateManagerImport]; + } + }, + }) as OnyxUpdateManagerMock; }); -const AppMock = App as AppMockType; -const OnyxUpdateManagerMock = OnyxUpdateManager as OnyxUpdateManagerMockType; +const App = AppImport as AppActionsMock; +const OnyxUpdateManager = OnyxUpdateManagerImport as OnyxUpdateManagerMock; let lastUpdateIDAppliedToClient = 0; Onyx.connect({ @@ -119,19 +127,19 @@ const mockUpdate5 = createMockUpdate(5); const mockUpdate6 = createMockUpdate(6); describe('OnyxUpdateManager', () => { - beforeEach(() => { - Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); + beforeEach(async () => { + // @ts-expect-error ignore invalid onyx key + await Onyx.set(lastAppliedMissingUpdateIDOnyxKey, 2); + await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); OnyxUpdateManager.resetDeferralLogicVariables(); }); - it('should fetch missing Onyx updates once, defer updates and apply after missing updates', async () => { - // @ts-expect-error ignore invalid onyx key - await Onyx.set(lastAppliedMissingUpdateIDKey, 1); + it('should fetch missing Onyx updates once, defer updates and apply after missing updates', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); - AppMock.getMissingOnyxUpdatesTriggeredPromise.then(() => { + App.getMissingOnyxUpdatesTriggeredPromise.then(() => { expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); }); @@ -145,12 +153,13 @@ describe('OnyxUpdateManager', () => { it('should only apply deferred updates that are after the locally applied update', async () => { // @ts-expect-error ignore invalid onyx key - await Onyx.set(lastAppliedMissingUpdateIDKey, 3); + await Onyx.set(lastAppliedMissingUpdateIDOnyxKey, 3); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); - AppMock.getMissingOnyxUpdatesTriggeredPromise.then(() => { + App.getMissingOnyxUpdatesTriggeredPromise.then(() => { expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); }); From 28004cb03c6e25052514ed8fa87dc9a861d4b3c9 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Sun, 21 Apr 2024 22:52:35 +0200 Subject: [PATCH 256/580] implement proxy for OnyxUpdateManager --- tests/unit/OnyxUpdateManagerTest.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 9fcf81850be9..3593770cac07 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -2,7 +2,6 @@ import Onyx from 'react-native-onyx'; import * as AppImport from '@libs/actions/App'; import * as OnyxUpdateManagerImport from '@libs/actions/OnyxUpdateManager'; import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager'; -// import {deferredUpdates, handleOnyxUpdateGap, queryPromise} from '@libs/actions/OnyxUpdateManager'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; @@ -143,7 +142,7 @@ describe('OnyxUpdateManager', () => { expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); }); - OnyxUpdateManager.queryPromise?.then(() => { + OnyxUpdateManager.queryPromise?.finally(() => { expect(lastUpdateIDAppliedToClient).toBe(4); expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/naming-convention @@ -164,7 +163,7 @@ describe('OnyxUpdateManager', () => { Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); }); - OnyxUpdateManager.queryPromise?.then(() => { + OnyxUpdateManager.queryPromise?.finally(() => { expect(lastUpdateIDAppliedToClient).toBe(6); expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/naming-convention From 841a270ac5a1a465ff6032fa8626035b30dcc339 Mon Sep 17 00:00:00 2001 From: Sibtain Ali <allroundexperts@gmail.com> Date: Mon, 22 Apr 2024 02:08:29 +0500 Subject: [PATCH 257/580] feat: use appBG colour for highlight --- src/hooks/useAnimatedHighlightStyle/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts index 2dbcf9b3c85a..027741d40b68 100644 --- a/src/hooks/useAnimatedHighlightStyle/index.ts +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -52,7 +52,7 @@ export default function useAnimatedHighlightStyle({ const theme = useTheme(); const highlightBackgroundStyle = useAnimatedStyle(() => ({ - backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), + backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [theme.appBG, theme.border]), height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]), opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), borderRadius, From 70e428ea38fe5f089c3516be190220403322513c Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Sun, 21 Apr 2024 23:24:56 +0200 Subject: [PATCH 258/580] update tests --- tests/unit/OnyxUpdateManagerTest.ts | 103 +++++++++++++++++++--------- 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 3593770cac07..4d7684a66251 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -4,6 +4,7 @@ import * as OnyxUpdateManagerImport from '@libs/actions/OnyxUpdateManager'; import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; const lastAppliedMissingUpdateIDOnyxKey = 'lastAppliedMissingUpdateID'; @@ -65,6 +66,22 @@ jest.mock('@libs/actions/OnyxUpdateManager', () => { const {promise: applyUpdatesTriggeredPromise, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggeredPromise} = createTriggerPromise(); + // return { + // ...OnyxUpdateManagerImplementation, + // applyUpdatesTriggeredPromise, + // applyUpdates: jest.fn((updates: DeferredUpdatesDictionary) => { + // applyUpdatesTriggered(); + + // const promise = OnyxUpdateManagerImplementation.applyUpdates(updates); + + // promise.finally(() => { + // resetApplyUpdatesTriggeredPromise(); + // }); + + // return promise; + // }), + // } as OnyxUpdateManagerMock; + return new Proxy(OnyxUpdateManagerImplementation, { get: (target, prop) => { switch (prop) { @@ -74,6 +91,8 @@ jest.mock('@libs/actions/OnyxUpdateManager', () => { return jest.fn((updates: DeferredUpdatesDictionary) => { applyUpdatesTriggered(); + console.log('apply updates triggered'); + const promise = OnyxUpdateManagerImplementation.applyUpdates(updates); promise.finally(() => { @@ -92,12 +111,6 @@ jest.mock('@libs/actions/OnyxUpdateManager', () => { const App = AppImport as AppActionsMock; const OnyxUpdateManager = OnyxUpdateManagerImport as OnyxUpdateManagerMock; -let lastUpdateIDAppliedToClient = 0; -Onyx.connect({ - key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), -}); - const createMockUpdate = (lastUpdateID: number): OnyxUpdatesFromServer => ({ type: 'https', lastUpdateID, @@ -125,32 +138,58 @@ const mockUpdate4 = createMockUpdate(4); const mockUpdate5 = createMockUpdate(5); const mockUpdate6 = createMockUpdate(6); +const resetOnyxUpdateManager = async () => { + // @ts-expect-error ignore invalid onyx key + await Onyx.set(lastAppliedMissingUpdateIDOnyxKey, 2); + await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); + OnyxUpdateManager.resetDeferralLogicVariables(); +}; + describe('OnyxUpdateManager', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + beforeEach(async () => { - // @ts-expect-error ignore invalid onyx key - await Onyx.set(lastAppliedMissingUpdateIDOnyxKey, 2); - await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); - OnyxUpdateManager.resetDeferralLogicVariables(); + jest.clearAllMocks(); + Onyx.clear(); + await resetOnyxUpdateManager(); + return waitForBatchedUpdates(); }); it('should fetch missing Onyx updates once, defer updates and apply after missing updates', () => { + let lastUpdateIDAppliedToClient = 0; + Onyx.connect({ + key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, + callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), + }); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); - App.getMissingOnyxUpdatesTriggeredPromise.then(() => { - expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); - }); - - OnyxUpdateManager.queryPromise?.finally(() => { - expect(lastUpdateIDAppliedToClient).toBe(4); - expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); - }); + return App.getMissingOnyxUpdatesTriggeredPromise + .then(() => { + expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); + }) + .then(waitForBatchedUpdates) + .then(() => OnyxUpdateManager.queryPromise) + ?.then(() => { + expect(lastUpdateIDAppliedToClient).toBe(5); + expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); + }); }); it('should only apply deferred updates that are after the locally applied update', async () => { + let lastUpdateIDAppliedToClient = 0; + Onyx.connect({ + key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, + callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), + }); + // @ts-expect-error ignore invalid onyx key await Onyx.set(lastAppliedMissingUpdateIDOnyxKey, 3); @@ -158,16 +197,18 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); - App.getMissingOnyxUpdatesTriggeredPromise.then(() => { - expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); - Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); - }); - - OnyxUpdateManager.queryPromise?.finally(() => { - expect(lastUpdateIDAppliedToClient).toBe(6); - expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); - }); + return App.getMissingOnyxUpdatesTriggeredPromise + .then(() => { + expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); + Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); + }) + .then(waitForBatchedUpdates) + .then(() => OnyxUpdateManager.queryPromise) + ?.then(() => { + expect(lastUpdateIDAppliedToClient).toBe(6); + expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); + }); }); }); From 8093ec7494e3d9d5fa1505bf36ac1de83b3becb9 Mon Sep 17 00:00:00 2001 From: Jon Dubielzyk <145416827+dubielzyk-expensify@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:35:46 +1000 Subject: [PATCH 259/580] Optically center icons ## Details Optically fixes icons: ## Fixed Issues $ Expensify/Expensify#389499 --- assets/images/Caret Left.svg | 10 ++++++++++ assets/images/Caret Right.svg | 10 ++++++++++ assets/images/Play.svg | 9 +++++++++ 3 files changed, 29 insertions(+) create mode 100644 assets/images/Caret Left.svg create mode 100644 assets/images/Caret Right.svg create mode 100644 assets/images/Play.svg diff --git a/assets/images/Caret Left.svg b/assets/images/Caret Left.svg new file mode 100644 index 000000000000..77534c1ffad6 --- /dev/null +++ b/assets/images/Caret Left.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> +<style type="text/css"> + .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#8B9C8F;} +</style> +<path class="st0" d="M14.2,18.4c-0.8,0.8-2,0.8-2.8,0L3,10l8.4-8.4c0.8-0.8,2-0.8,2.8,0c0.8,0.8,0.8,2,0,2.8L8.7,10l5.6,5.6 + C15,16.4,15,17.6,14.2,18.4z"/> +</svg> diff --git a/assets/images/Caret Right.svg b/assets/images/Caret Right.svg new file mode 100644 index 000000000000..d4d1d90ea3ed --- /dev/null +++ b/assets/images/Caret Right.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> +<style type="text/css"> + .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#8B9C8F;} +</style> +<path class="st0" d="M5.6,18.4c0.8,0.8,2,0.8,2.8,0l8.4-8.4L8.4,1.6c-0.8-0.8-2-0.8-2.8,0c-0.8,0.8-0.8,2,0,2.8l5.6,5.6l-5.6,5.6 + C4.8,16.4,4.8,17.6,5.6,18.4z"/> +</svg> diff --git a/assets/images/Play.svg b/assets/images/Play.svg new file mode 100644 index 000000000000..583ad9669de7 --- /dev/null +++ b/assets/images/Play.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#8B9C8F;} +</style> +<path class="st0" d="M7.2,17.9c-1.7,1-3.7-0.3-3.7-2.2V4.3c0-1.9,2.1-3.1,3.7-2.2l10,5.7c1.7,1,1.7,3.4,0,4.3L7.2,17.9z"/> +</svg> From d4b4d7a920d09de473dec953c620272d1ffcc64d Mon Sep 17 00:00:00 2001 From: Jon Dubielzyk <jon@expensify.com> Date: Mon, 22 Apr 2024 14:55:48 +1000 Subject: [PATCH 260/580] Fix naming and remove color --- assets/images/Caret Left.svg | 10 ---------- assets/images/Caret Right.svg | 10 ---------- assets/images/Play.svg | 5 +---- assets/images/arrow-right.svg | 11 ++++++++++- assets/images/back-left.svg | 11 ++++++++++- assets/images/play.svg | 7 ++++++- 6 files changed, 27 insertions(+), 27 deletions(-) delete mode 100644 assets/images/Caret Left.svg delete mode 100644 assets/images/Caret Right.svg diff --git a/assets/images/Caret Left.svg b/assets/images/Caret Left.svg deleted file mode 100644 index 77534c1ffad6..000000000000 --- a/assets/images/Caret Left.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> -<style type="text/css"> - .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#8B9C8F;} -</style> -<path class="st0" d="M14.2,18.4c-0.8,0.8-2,0.8-2.8,0L3,10l8.4-8.4c0.8-0.8,2-0.8,2.8,0c0.8,0.8,0.8,2,0,2.8L8.7,10l5.6,5.6 - C15,16.4,15,17.6,14.2,18.4z"/> -</svg> diff --git a/assets/images/Caret Right.svg b/assets/images/Caret Right.svg deleted file mode 100644 index d4d1d90ea3ed..000000000000 --- a/assets/images/Caret Right.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> -<style type="text/css"> - .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#8B9C8F;} -</style> -<path class="st0" d="M5.6,18.4c0.8,0.8,2,0.8,2.8,0l8.4-8.4L8.4,1.6c-0.8-0.8-2-0.8-2.8,0c-0.8,0.8-0.8,2,0,2.8l5.6,5.6l-5.6,5.6 - C4.8,16.4,4.8,17.6,5.6,18.4z"/> -</svg> diff --git a/assets/images/Play.svg b/assets/images/Play.svg index 583ad9669de7..5f7e14969529 100644 --- a/assets/images/Play.svg +++ b/assets/images/Play.svg @@ -2,8 +2,5 @@ <!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> -<style type="text/css"> - .st0{fill:#8B9C8F;} -</style> -<path class="st0" d="M7.2,17.9c-1.7,1-3.7-0.3-3.7-2.2V4.3c0-1.9,2.1-3.1,3.7-2.2l10,5.7c1.7,1,1.7,3.4,0,4.3L7.2,17.9z"/> +<path d="M7.2,17.9c-1.7,1-3.7-0.3-3.7-2.2V4.3c0-1.9,2.1-3.1,3.7-2.2l10,5.7c1.7,1,1.7,3.4,0,4.3L7.2,17.9z"/> </svg> diff --git a/assets/images/arrow-right.svg b/assets/images/arrow-right.svg index df13c75ca414..8d2ded92e791 100644 --- a/assets/images/arrow-right.svg +++ b/assets/images/arrow-right.svg @@ -1 +1,10 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd}</style><path d="M5.6,1.6c0.8-0.8,2-0.8,2.8,0l8.4,8.4l-8.4,8.4c-0.8,0.8-2,0.8-2.8,0c-0.8-0.8-0.8-2,0-2.8l5.6-5.6L5.6,4.4 C4.8,3.6,4.8,2.4,5.6,1.6z" class="st0"/></svg> \ No newline at end of file +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> +<style type="text/css"> + .st0{fill-rule:evenodd;clip-rule:evenodd;} +</style> +<path class="st0" d="M5.6,18.4c0.8,0.8,2,0.8,2.8,0l8.4-8.4L8.4,1.6c-0.8-0.8-2-0.8-2.8,0s-0.8,2,0,2.8l5.6,5.6l-5.6,5.6 + C4.8,16.4,4.8,17.6,5.6,18.4z"/> +</svg> diff --git a/assets/images/back-left.svg b/assets/images/back-left.svg index 51164100ff59..2ddd554e9720 100644 --- a/assets/images/back-left.svg +++ b/assets/images/back-left.svg @@ -1 +1,10 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd}</style><path d="M14.4,1.6c-0.8-0.8-2-0.8-2.8,0L3.2,10l8.4,8.4c0.8,0.8,2,0.8,2.8,0c0.8-0.8,0.8-2,0-2.8L8.8,10l5.6-5.6 C15.2,3.6,15.2,2.4,14.4,1.6z" class="st0"/></svg> \ No newline at end of file +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> +<style type="text/css"> + .st0{fill-rule:evenodd;clip-rule:evenodd;} +</style> +<path class="st0" d="M14.2,18.4c-0.8,0.8-2,0.8-2.8,0L3,10l8.4-8.4c0.8-0.8,2-0.8,2.8,0s0.8,2,0,2.8L8.7,10l5.6,5.6 + C15,16.4,15,17.6,14.2,18.4z"/> +</svg> diff --git a/assets/images/play.svg b/assets/images/play.svg index cb781459e44e..5f7e14969529 100644 --- a/assets/images/play.svg +++ b/assets/images/play.svg @@ -1 +1,6 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 20 20" style="enable-background:new 0 0 20 20" xml:space="preserve"><path d="M6.24035 17.8626C4.5737 18.815 2.5 17.6115 2.5 15.692L2.5 4.30789C2.5 2.38833 4.57371 1.18492 6.24035 2.13729L16.2014 7.82936C17.881 8.78909 17.881 11.2108 16.2014 12.1706L6.24035 17.8626Z"/></svg> \ No newline at end of file +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> +<path d="M7.2,17.9c-1.7,1-3.7-0.3-3.7-2.2V4.3c0-1.9,2.1-3.1,3.7-2.2l10,5.7c1.7,1,1.7,3.4,0,4.3L7.2,17.9z"/> +</svg> From 43aeab5b1e46770d706158ea3f113194581a951d Mon Sep 17 00:00:00 2001 From: Jon Dubielzyk <145416827+dubielzyk-expensify@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:56:43 +1000 Subject: [PATCH 261/580] Delete Play icon --- assets/images/Play.svg | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 assets/images/Play.svg diff --git a/assets/images/Play.svg b/assets/images/Play.svg deleted file mode 100644 index 5f7e14969529..000000000000 --- a/assets/images/Play.svg +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> -<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" - viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> -<path d="M7.2,17.9c-1.7,1-3.7-0.3-3.7-2.2V4.3c0-1.9,2.1-3.1,3.7-2.2l10,5.7c1.7,1,1.7,3.4,0,4.3L7.2,17.9z"/> -</svg> From cbacc83fb5c206d5d1fd9c39ccaae6a4619de3d1 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Mon, 22 Apr 2024 13:24:47 +0700 Subject: [PATCH 262/580] edit amount for payer --- src/components/MoneyRequestConfirmationList.tsx | 14 ++++++++------ src/libs/OptionsListUtils.ts | 5 +++-- src/libs/actions/IOU.ts | 12 ++++++++++-- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 19090071fe58..b98726242475 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -382,13 +382,14 @@ function MoneyRequestConfirmationList({ * Returns the participants with amount */ const getParticipantsWithAmount = useCallback( - (participantsList: Participant[]) => { + (participantsList: Participant[], payerAccountID: number) => { const amount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? ''); - const myAmount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? '', true); + const payerAmount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? '', true); return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '', - myAmount > 0 ? CurrencyUtils.convertToDisplayString(myAmount, iouCurrencyCode) : '', + payerAmount > 0 ? CurrencyUtils.convertToDisplayString(payerAmount, iouCurrencyCode) : '', + payerAccountID, ); }, [iouAmount, iouCurrencyCode], @@ -438,7 +439,7 @@ function MoneyRequestConfirmationList({ const sections = []; const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants, payeePersonalDetails.accountID); let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; if (!canModifyParticipants) { @@ -465,7 +466,7 @@ function MoneyRequestConfirmationList({ }); } return sections; - }, [selectedParticipants, selectedParticipantsProp, hasMultipleParticipants, getParticipantsWithAmount, translate, canModifyParticipants]); + }, [selectedParticipants, selectedParticipantsProp, hasMultipleParticipants, getParticipantsWithAmount, translate, canModifyParticipants, payeePersonalDetails.accountID]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { @@ -603,7 +604,7 @@ function MoneyRequestConfirmationList({ playSound(SOUNDS.DONE); setDidConfirm(true); - onConfirm?.(selectedParticipants); + onConfirm?.(selectedParticipants.filter((participant) => participant.accountID !== currentUserPersonalDetails.accountID)); } }, [ @@ -621,6 +622,7 @@ function MoneyRequestConfirmationList({ iouAmount, isEditingSplitBill, onConfirm, + currentUserPersonalDetails.accountID, ], ); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index ce61cf06f062..ccbdbe56cb53 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1978,11 +1978,12 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person function getIOUConfirmationOptionsFromParticipants( participants: Array<Participant | ReportUtils.OptionData>, amountText: string, - myAmountText = '', + payerAmountText: string, + payerAccountID: number, ): Array<Participant | ReportUtils.OptionData> { return participants.map((participant) => ({ ...participant, - descriptiveText: participant.accountID === currentUserAccountID ? myAmountText : amountText, + descriptiveText: participant.accountID === payerAccountID ? payerAmountText : amountText, })); } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8ab4eb676633..b4ec985cdbd7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3127,6 +3127,7 @@ function createSplitsAndOnyxData( existingSplitChatReportID = '', billable = false, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, + splitPayerAccountIDs: number[] = [], ): SplitsAndOnyxData { const currentUserEmailForIOUSplit = PhoneNumber.addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = participants.map((participant) => Number(participant.accountID)); @@ -3292,11 +3293,17 @@ function createSplitsAndOnyxData( } // Loop through participants creating individual chats, iouReports and reportActionIDs as needed - const splitAmount = IOUUtils.calculateAmount(participants.length, amount, currency, false); - const splits: Split[] = [{email: currentUserEmailForIOUSplit, accountID: currentUserAccountID, amount: IOUUtils.calculateAmount(participants.length, amount, currency, true)}]; + const splits: Split[] = [ + { + email: currentUserEmailForIOUSplit, + accountID: currentUserAccountID, + amount: IOUUtils.calculateAmount(participants.length, amount, currency, splitPayerAccountIDs.includes(currentUserAccountID)), + }, + ]; const hasMultipleParticipants = participants.length > 1; participants.forEach((participant) => { + const splitAmount = IOUUtils.calculateAmount(participants.length, amount, currency, splitPayerAccountIDs.includes(participant.accountID ?? 0)); // In a case when a participant is a workspace, even when a current user is not an owner of the workspace const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(participant); @@ -3526,6 +3533,7 @@ function splitBill({ existingSplitChatReportID, billable, iouRequestType, + splitPayerAccoutIDs, ); const parameters: SplitBillParams = { From e3608124f1b138d574be3587c3da10bdf4b2fbbc Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Mon, 22 Apr 2024 14:12:12 +0700 Subject: [PATCH 263/580] fix update payer to participant --- src/components/MoneyRequestConfirmationList.tsx | 1 + src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b98726242475..70063ced9f2e 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -688,6 +688,7 @@ function MoneyRequestConfirmationList({ { item: ( <MenuItem + key={translate('moneyRequestConfirmationList.paidBy')} label={translate('moneyRequestConfirmationList.paidBy')} interactive={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate} description={payeePersonalDetails.login} diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 79a892b816fa..4299893ac9c3 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -120,7 +120,7 @@ function IOURequestStepConfirmation({ return; } - const payeeParticipant = OptionsListUtils.getParticipantsOption({accountID: payeePersonalDetails?.accountID, selected: true}, personalDetails); + const payeeParticipant: Participant = {accountID: payeePersonalDetails?.accountID, selected: true}; IOU.setMoneyRequestParticipants_temporaryForRefactor(transaction.transactionID, [payeeParticipant, ...(transaction?.participants ?? [])]); // We only want to run it when the component is mounted From a6264ec233da2cd2100b29ead8a14b541e7ef3b1 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Mon, 22 Apr 2024 14:17:15 +0700 Subject: [PATCH 264/580] not show right icon of split payer when opening split detail page --- src/components/MoneyRequestConfirmationList.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 70063ced9f2e..59d400735c0a 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -690,14 +690,14 @@ function MoneyRequestConfirmationList({ <MenuItem key={translate('moneyRequestConfirmationList.paidBy')} label={translate('moneyRequestConfirmationList.paidBy')} - interactive={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate} + interactive={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate && !isReadOnly} description={payeePersonalDetails.login} title={payeePersonalDetails.displayName} icon={payeeIcons} onPress={() => { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); }} - shouldShowRightIcon={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate} + shouldShowRightIcon={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate && !isReadOnly} titleWithTooltips={payeeTooltipDetails} /> ), From 86182415218ba3136fa9e12369d65a35a7666638 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 22 Apr 2024 09:24:44 +0200 Subject: [PATCH 265/580] Update sender logic to work with updates in main --- src/components/MoneyRequestConfirmationList.tsx | 2 +- src/libs/actions/IOU.ts | 5 +++-- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 1 + src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 2 +- src/pages/iou/request/step/IOURequestStepSendFrom.tsx | 1 + src/types/onyx/IOU.ts | 1 + 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index ece975429033..6062363c22b5 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -283,7 +283,7 @@ function MoneyRequestConfirmationList({ const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); const senderWorkspace = useMemo(() => { - const senderWorkspaceParticipant = selectedParticipantsProp.find((participant) => participant.policyID); + const senderWorkspaceParticipant = selectedParticipantsProp.find((participant) => participant.isSender); return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${senderWorkspaceParticipant?.policyID}`]; }, [allPolicies, selectedParticipantsProp]); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9b1c419c1b18..338cb8d82359 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1615,7 +1615,7 @@ function getSendInvoiceInformation( ): SendInvoiceInformation { const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); - const senderWorkspaceID = participants?.find((participant) => participant?.policyID)?.policyID ?? ''; + const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID ?? ''; const receiverParticipant = participants?.find((participant) => participant?.accountID); const receiverAccountID = receiverParticipant?.accountID ?? -1; let receiver = ReportUtils.getPersonalDetailsForAccountID(receiverAccountID); @@ -6295,6 +6295,7 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On ...participants, { policyID: chatReport?.policyID, + isSender: true, selected: false, }, ]; @@ -6490,7 +6491,7 @@ function savePreferredPaymentMethod(policyID: string, paymentMethod: PaymentMeth /** Get report policy id of IOU request */ function getIOURequestPolicyID(transaction: OnyxEntry<OnyxTypes.Transaction>, report: OnyxEntry<OnyxTypes.Report>): string { // Workspace sender will exist for invoices - const workspaceSender = transaction?.participants?.find((participant) => participant.policyID); + const workspaceSender = transaction?.participants?.find((participant) => participant.isSender); return workspaceSender?.policyID ?? report?.policyID ?? '0'; } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 15c70f3e19ff..d758004c9e7f 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -205,6 +205,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({participants, onF newParticipants.push({ policyID: primaryPolicy.id, + isSender: true, selected: false, iouType, }); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 840284254213..151b6a796f9a 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -111,7 +111,7 @@ function IOURequestStepConfirmation({ transaction?.participants?.map((participant) => { const participantAccountID = participant.accountID ?? 0; - if (participant.policyID && iouType === CONST.IOU.TYPE.INVOICE) { + if (participant.isSender && iouType === CONST.IOU.TYPE.INVOICE) { return participant; } diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index da0448235036..a64fe2b58054 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -70,6 +70,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte newParticipants.push({ policyID: item.value, + isSender: true, selected: false, }); diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index 7e1827f73954..726b94c5f6d3 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -22,6 +22,7 @@ type Participant = { text?: string; isSelected?: boolean; isSelfDM?: boolean; + isSender?: boolean; }; type Split = { From d726f3f254e239a57dfeb26cc172350a30c3c0d2 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Mon, 22 Apr 2024 14:53:43 +0700 Subject: [PATCH 266/580] add explaination comment --- src/components/OptionListContextProvider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/OptionListContextProvider.tsx b/src/components/OptionListContextProvider.tsx index 473c0f5bb083..f823c5410ae6 100644 --- a/src/components/OptionListContextProvider.tsx +++ b/src/components/OptionListContextProvider.tsx @@ -153,6 +153,7 @@ function OptionsListContextProvider({reports, children}: OptionsListProviderProp return newOptions; }); + // This effect is used to update the options list when personal details change so we ignore all dependencies except personalDetails // eslint-disable-next-line react-hooks/exhaustive-deps }, [personalDetails]); From 5ca73f1774a74e8aa44e558b5a47f06d479405c1 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 22 Apr 2024 10:05:19 +0200 Subject: [PATCH 267/580] Lint fix --- src/pages/iou/request/IOURequestStartPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index aadd07565cff..9183899ea80c 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -101,7 +101,8 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT && iouType !== CONST.IOU.TYPE.INVOICE; + const shouldDisplayDistanceRequest = + (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT && iouType !== CONST.IOU.TYPE.INVOICE; const shouldDisplayScanRequest = iouType !== CONST.IOU.TYPE.INVOICE; // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense From a5d58227a5654c1d330e70b812e58482ef392924 Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Mon, 22 Apr 2024 10:27:13 +0200 Subject: [PATCH 268/580] fix eslint --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index db892131c792..328ed8548975 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -30,7 +30,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From 089069614f68742bd7a99a895189fda5cf3b4b78 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 22 Apr 2024 10:55:38 +0200 Subject: [PATCH 269/580] extract util logic from OnyxUpdateManager --- .../index.ts} | 129 +----------------- src/libs/actions/OnyxUpdateManager/types.ts | 5 + src/libs/actions/OnyxUpdateManager/utils.ts | 129 ++++++++++++++++++ 3 files changed, 141 insertions(+), 122 deletions(-) rename src/libs/actions/{OnyxUpdateManager.ts => OnyxUpdateManager/index.ts} (50%) create mode 100644 src/libs/actions/OnyxUpdateManager/types.ts create mode 100644 src/libs/actions/OnyxUpdateManager/utils.ts diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager/index.ts similarity index 50% rename from src/libs/actions/OnyxUpdateManager.ts rename to src/libs/actions/OnyxUpdateManager/index.ts index 66bfccde89cb..176fd10899fb 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager/index.ts @@ -3,11 +3,12 @@ import Onyx from 'react-native-onyx'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; +import * as App from '@userActions/App'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer, Response} from '@src/types/onyx'; import {isValidOnyxUpdateFromServer} from '@src/types/onyx/OnyxUpdatesFromServer'; -import * as App from './App'; -import * as OnyxUpdates from './OnyxUpdates'; +import type DeferredUpdatesDictionary from './types'; +import * as OnyxUpdateManagerUtils from './utils'; // This file is in charge of looking at the updateIDs coming from the server and comparing them to the last updateID that the client has. // If the client is behind the server, then we need to @@ -41,7 +42,6 @@ Onyx.connect({ // eslint-disable-next-line import/no-mutable-exports let queryPromise: Promise<Response | Response[] | void> | undefined; -type DeferredUpdatesDictionary = Record<number, OnyxUpdatesFromServer>; // eslint-disable-next-line import/no-mutable-exports let deferredUpdates: DeferredUpdatesDictionary = {}; @@ -58,123 +58,6 @@ function finalizeUpdatesAndResumeQueue() { SequentialQueue.unpause(); } -// This function applies a list of updates to Onyx in order and resolves when all updates have been applied -const applyUpdates = (updates: DeferredUpdatesDictionary) => Promise.all(Object.values(updates).map((update) => OnyxUpdates.apply(update))); - -// In order for the deferred updates to be applied correctly in order, -// we need to check if there are any gaps between deferred updates. -type DetectGapAndSplitResult = {applicableUpdates: DeferredUpdatesDictionary; updatesAfterGaps: DeferredUpdatesDictionary; latestMissingUpdateID: number | undefined}; -function detectGapsAndSplit(updates: DeferredUpdatesDictionary): DetectGapAndSplitResult { - const updateValues = Object.values(updates); - const applicableUpdates: DeferredUpdatesDictionary = {}; - - let gapExists = false; - let firstUpdateAfterGaps: number | undefined; - let latestMissingUpdateID: number | undefined; - - for (const [index, update] of updateValues.entries()) { - const isFirst = index === 0; - - // If any update's previousUpdateID doesn't match the lastUpdateID from the previous update, the deferred updates aren't chained and there's a gap. - // For the first update, we need to check that the previousUpdateID of the fetched update is the same as the lastUpdateIDAppliedToClient. - // For any other updates, we need to check if the previousUpdateID of the current update is found in the deferred updates. - // If an update is chained, we can add it to the applicable updates. - const isChained = isFirst ? update.previousUpdateID === lastUpdateIDAppliedToClient : !!updates[Number(update.previousUpdateID)]; - if (isChained) { - // If a gap exists already, we will not add any more updates to the applicable updates. - // Instead, once there are two chained updates again, we can set "firstUpdateAfterGaps" to the first update after the current gap. - if (gapExists) { - // If "firstUpdateAfterGaps" hasn't been set yet and there was a gap, we need to set it to the first update after all gaps. - if (!firstUpdateAfterGaps) { - firstUpdateAfterGaps = Number(update.previousUpdateID); - } - } else { - // If no gap exists yet, we can add the update to the applicable updates - applicableUpdates[Number(update.lastUpdateID)] = update; - } - } else { - // When we find a (new) gap, we need to set "gapExists" to true and reset the "firstUpdateAfterGaps" variable, - // so that we can continue searching for the next update after all gaps - gapExists = true; - firstUpdateAfterGaps = undefined; - - // If there is a gap, it means the previous update is the latest missing update. - latestMissingUpdateID = Number(update.previousUpdateID); - } - } - - // When "firstUpdateAfterGaps" is not set yet, we need to set it to the last update in the list, - // because we will fetch all missing updates up to the previous one and can then always apply the last update in the deferred updates. - if (!firstUpdateAfterGaps) { - firstUpdateAfterGaps = Number(updateValues[updateValues.length - 1].lastUpdateID); - } - - let updatesAfterGaps: DeferredUpdatesDictionary = {}; - if (gapExists) { - updatesAfterGaps = Object.entries(updates).reduce<DeferredUpdatesDictionary>( - (accUpdates, [lastUpdateID, update]) => ({ - ...accUpdates, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ...(Number(lastUpdateID) >= firstUpdateAfterGaps ? {[Number(lastUpdateID)]: update} : {}), - }), - {}, - ); - } - - return {applicableUpdates, updatesAfterGaps, latestMissingUpdateID}; -} - -// This function will check for gaps in the deferred updates and -// apply the updates in order after the missing updates are fetched and applied -function validateAndApplyDeferredUpdates(): Promise<Response[] | void> { - // We only want to apply deferred updates that are newer than the last update that was applied to the client. - // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. - const pendingDeferredUpdates = Object.entries(deferredUpdates).reduce<DeferredUpdatesDictionary>( - (accUpdates, [lastUpdateID, update]) => ({ - ...accUpdates, - ...(Number(lastUpdateID) > lastUpdateIDAppliedToClient ? {[Number(lastUpdateID)]: update} : {}), - }), - {}, - ); - - // If there are no remaining deferred updates after filtering out outdated ones, - // we can just unpause the queue and return - if (Object.values(pendingDeferredUpdates).length === 0) { - return Promise.resolve(); - } - - const {applicableUpdates, updatesAfterGaps, latestMissingUpdateID} = detectGapsAndSplit(pendingDeferredUpdates); - - // If we detected a gap in the deferred updates, only apply the deferred updates before the gap, - // re-fetch the missing updates and then apply the remaining deferred updates after the gap - if (latestMissingUpdateID) { - return new Promise((resolve, reject) => { - deferredUpdates = {}; - - applyUpdates(applicableUpdates).then(() => { - // After we have applied the applicable updates, there might have been new deferred updates added. - // In the next (recursive) call of "validateAndApplyDeferredUpdates", - // the initial "updatesAfterGaps" and all new deferred updates will be applied in order, - // as long as there was no new gap detected. Otherwise repeat the process. - deferredUpdates = {...deferredUpdates, ...updatesAfterGaps}; - - // It should not be possible for lastUpdateIDAppliedToClient to be null, therefore we can ignore this case. - // If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates. - if (latestMissingUpdateID <= lastUpdateIDAppliedToClient) { - validateAndApplyDeferredUpdates().then(resolve).catch(reject); - return; - } - - // Then we can fetch the missing updates and apply them - App.getMissingOnyxUpdates(lastUpdateIDAppliedToClient, latestMissingUpdateID).then(validateAndApplyDeferredUpdates).then(resolve).catch(reject); - }); - }); - } - - // If there are no gaps in the deferred updates, we can apply all deferred updates in order - return applyUpdates(applicableUpdates); -} - /** * * @param onyxUpdatesFromServer @@ -251,7 +134,9 @@ function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromSer // Get the missing Onyx updates from the server and afterwards validate and apply the deferred updates. // This will trigger recursive calls to "validateAndApplyDeferredUpdates" if there are gaps in the deferred updates. - queryPromise = App.getMissingOnyxUpdates(lastUpdateIDFromClient, previousUpdateIDFromServer).then(validateAndApplyDeferredUpdates); + queryPromise = App.getMissingOnyxUpdates(lastUpdateIDFromClient, previousUpdateIDFromServer) + .then(() => OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates(deferredUpdates, lastUpdateIDFromClient)) + .then((newDeferredUpdates) => (deferredUpdates = newDeferredUpdates)); } queryPromise.finally(finalizeUpdatesAndResumeQueue); @@ -266,5 +151,5 @@ export default () => { }; export {handleOnyxUpdateGap}; -export {queryPromise, deferredUpdates, applyUpdates, resetDeferralLogicVariables}; +export {queryPromise, deferredUpdates, resetDeferralLogicVariables}; export type {DeferredUpdatesDictionary}; diff --git a/src/libs/actions/OnyxUpdateManager/types.ts b/src/libs/actions/OnyxUpdateManager/types.ts new file mode 100644 index 000000000000..a081dff00665 --- /dev/null +++ b/src/libs/actions/OnyxUpdateManager/types.ts @@ -0,0 +1,5 @@ +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; + +type DeferredUpdatesDictionary = Record<number, OnyxUpdatesFromServer>; + +export default DeferredUpdatesDictionary; diff --git a/src/libs/actions/OnyxUpdateManager/utils.ts b/src/libs/actions/OnyxUpdateManager/utils.ts new file mode 100644 index 000000000000..bdbcfa13e37a --- /dev/null +++ b/src/libs/actions/OnyxUpdateManager/utils.ts @@ -0,0 +1,129 @@ +import * as App from '@userActions/App'; +import * as OnyxUpdates from '@userActions/OnyxUpdates'; +import type DeferredUpdatesDictionary from './types'; + +// This function applies a list of updates to Onyx in order and resolves when all updates have been applied +const applyUpdates = (updates: DeferredUpdatesDictionary) => Promise.all(Object.values(updates).map((update) => OnyxUpdates.apply(update))); + +// In order for the deferred updates to be applied correctly in order, +// we need to check if there are any gaps between deferred updates. +type DetectGapAndSplitResult = {applicableUpdates: DeferredUpdatesDictionary; updatesAfterGaps: DeferredUpdatesDictionary; latestMissingUpdateID: number | undefined}; +function detectGapsAndSplit(updates: DeferredUpdatesDictionary, lastUpdateIDFromClient: number): DetectGapAndSplitResult { + const updateValues = Object.values(updates); + const applicableUpdates: DeferredUpdatesDictionary = {}; + + let gapExists = false; + let firstUpdateAfterGaps: number | undefined; + let latestMissingUpdateID: number | undefined; + + for (const [index, update] of updateValues.entries()) { + const isFirst = index === 0; + + // If any update's previousUpdateID doesn't match the lastUpdateID from the previous update, the deferred updates aren't chained and there's a gap. + // For the first update, we need to check that the previousUpdateID of the fetched update is the same as the lastUpdateIDAppliedToClient. + // For any other updates, we need to check if the previousUpdateID of the current update is found in the deferred updates. + // If an update is chained, we can add it to the applicable updates. + const isChained = isFirst ? update.previousUpdateID === lastUpdateIDFromClient : !!updates[Number(update.previousUpdateID)]; + if (isChained) { + // If a gap exists already, we will not add any more updates to the applicable updates. + // Instead, once there are two chained updates again, we can set "firstUpdateAfterGaps" to the first update after the current gap. + if (gapExists) { + // If "firstUpdateAfterGaps" hasn't been set yet and there was a gap, we need to set it to the first update after all gaps. + if (!firstUpdateAfterGaps) { + firstUpdateAfterGaps = Number(update.previousUpdateID); + } + } else { + // If no gap exists yet, we can add the update to the applicable updates + applicableUpdates[Number(update.lastUpdateID)] = update; + } + } else { + // When we find a (new) gap, we need to set "gapExists" to true and reset the "firstUpdateAfterGaps" variable, + // so that we can continue searching for the next update after all gaps + gapExists = true; + firstUpdateAfterGaps = undefined; + + // If there is a gap, it means the previous update is the latest missing update. + latestMissingUpdateID = Number(update.previousUpdateID); + } + } + + // When "firstUpdateAfterGaps" is not set yet, we need to set it to the last update in the list, + // because we will fetch all missing updates up to the previous one and can then always apply the last update in the deferred updates. + if (!firstUpdateAfterGaps) { + firstUpdateAfterGaps = Number(updateValues[updateValues.length - 1].lastUpdateID); + } + + let updatesAfterGaps: DeferredUpdatesDictionary = {}; + if (gapExists) { + updatesAfterGaps = Object.entries(updates).reduce<DeferredUpdatesDictionary>( + (accUpdates, [lastUpdateID, update]) => ({ + ...accUpdates, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ...(Number(lastUpdateID) >= firstUpdateAfterGaps ? {[Number(lastUpdateID)]: update} : {}), + }), + {}, + ); + } + + return {applicableUpdates, updatesAfterGaps, latestMissingUpdateID}; +} + +// This function will check for gaps in the deferred updates and +// apply the updates in order after the missing updates are fetched and applied +function validateAndApplyDeferredUpdates(deferredUpdates: DeferredUpdatesDictionary, lastUpdateIDFromClient: number): Promise<DeferredUpdatesDictionary> { + let newDeferredUpdates = {...deferredUpdates}; + + // We only want to apply deferred updates that are newer than the last update that was applied to the client. + // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. + const pendingDeferredUpdates = Object.entries(newDeferredUpdates).reduce<DeferredUpdatesDictionary>( + (accUpdates, [lastUpdateID, update]) => ({ + ...accUpdates, + ...(Number(lastUpdateID) > lastUpdateIDFromClient ? {[Number(lastUpdateID)]: update} : {}), + }), + {}, + ); + + // If there are no remaining deferred updates after filtering out outdated ones, + // we can just unpause the queue and return + if (Object.values(pendingDeferredUpdates).length === 0) { + return Promise.resolve(newDeferredUpdates); + } + + const {applicableUpdates, updatesAfterGaps, latestMissingUpdateID} = detectGapsAndSplit(pendingDeferredUpdates, lastUpdateIDFromClient); + + // If we detected a gap in the deferred updates, only apply the deferred updates before the gap, + // re-fetch the missing updates and then apply the remaining deferred updates after the gap + if (latestMissingUpdateID) { + return new Promise((resolve, reject) => { + newDeferredUpdates = {}; + + applyUpdates(applicableUpdates).then(() => { + // After we have applied the applicable updates, there might have been new deferred updates added. + // In the next (recursive) call of "validateAndApplyDeferredUpdates", + // the initial "updatesAfterGaps" and all new deferred updates will be applied in order, + // as long as there was no new gap detected. Otherwise repeat the process. + newDeferredUpdates = {...newDeferredUpdates, ...updatesAfterGaps}; + + // It should not be possible for lastUpdateIDAppliedToClient to be null, therefore we can ignore this case. + // If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates. + if (latestMissingUpdateID <= lastUpdateIDFromClient) { + validateAndApplyDeferredUpdates(newDeferredUpdates, lastUpdateIDFromClient) + .then(() => resolve(newDeferredUpdates)) + .catch(reject); + return; + } + + // Then we can fetch the missing updates and apply them + App.getMissingOnyxUpdates(lastUpdateIDFromClient, latestMissingUpdateID) + .then(() => validateAndApplyDeferredUpdates(newDeferredUpdates, lastUpdateIDFromClient)) + .then(() => resolve(newDeferredUpdates)) + .catch(reject); + }); + }); + } + + // If there are no gaps in the deferred updates, we can apply all deferred updates in order + return applyUpdates(applicableUpdates).then(() => newDeferredUpdates); +} + +export {applyUpdates, detectGapsAndSplit, validateAndApplyDeferredUpdates}; From 55bed2d23b383f5f63fb597b440860cbb5bce5d1 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Mon, 22 Apr 2024 11:58:23 +0200 Subject: [PATCH 270/580] fix PR comments --- src/ROUTES.ts | 4 +-- .../Wallet/ActivatePhysicalCardPage.tsx | 28 +++++++++---------- .../Wallet/Card/BaseGetPhysicalCard.tsx | 12 ++++---- .../settings/Wallet/ExpensifyCardPage.tsx | 13 +++++++-- .../settings/Wallet/PaymentMethodList.tsx | 5 ++-- 5 files changed, 34 insertions(+), 28 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ad03c157492a..c88c90847d0e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -92,8 +92,8 @@ const ROUTES = { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { - route: 'settings/wallet/card/:domain/:cardID', - getRoute: (domain: string, cardID: string) => `settings/wallet/card/${domain}/${cardID}` as const, + route: 'settings/wallet/card/:domain/:cardID?', + getRoute: (domain: string, cardID?: string) => (cardID ? (`settings/wallet/card/${domain}/${cardID}` as const) : (`settings/wallet/card/${domain}` as const)), }, SETTINGS_REPORT_FRAUD: { route: 'settings/wallet/card/:domain/:cardID/report-virtual-fraud', diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index 6dc3b5d168c4..ceacb2797724 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -56,8 +56,8 @@ function ActivatePhysicalCardPage({ const [lastPressedDigit, setLastPressedDigit] = useState(''); const domainCards = CardUtils.getDomainCards(cardList)[domain] ?? []; - const physicalCard = domainCards.find((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED); - const cardError = ErrorUtils.getLatestErrorMessage(physicalCard ?? {}); + const inactiveCard = domainCards.find((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED); + const cardError = ErrorUtils.getLatestErrorMessage(inactiveCard ?? {}); const activateCardCodeInputRef = useRef<MagicCodeInputHandle>(null); @@ -65,21 +65,21 @@ function ActivatePhysicalCardPage({ * If state of the card is CONST.EXPENSIFY_CARD.STATE.OPEN, navigate to card details screen. */ useEffect(() => { - if (physicalCard?.state !== CONST.EXPENSIFY_CARD.STATE.OPEN || physicalCard?.isLoading) { + if (inactiveCard?.state !== CONST.EXPENSIFY_CARD.STATE.OPEN || inactiveCard?.isLoading) { return; } Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID)); - }, [cardID, cardList, domain, physicalCard?.isLoading, physicalCard?.state]); + }, [cardID, cardList, domain, inactiveCard?.isLoading, inactiveCard?.state]); useEffect( () => () => { - if (!physicalCard?.cardID) { + if (!inactiveCard?.cardID) { return; } - CardSettings.clearCardListErrors(physicalCard?.cardID); + CardSettings.clearCardListErrors(inactiveCard?.cardID); }, - [physicalCard?.cardID], + [inactiveCard?.cardID], ); /** @@ -96,8 +96,8 @@ function ActivatePhysicalCardPage({ const onCodeInput = (text: string) => { setFormError(''); - if (cardError && physicalCard?.cardID) { - CardSettings.clearCardListErrors(physicalCard?.cardID); + if (cardError && inactiveCard?.cardID) { + CardSettings.clearCardListErrors(inactiveCard?.cardID); } setLastFourDigits(text); @@ -110,14 +110,14 @@ function ActivatePhysicalCardPage({ setFormError('activateCardPage.error.thatDidntMatch'); return; } - if (physicalCard?.cardID === undefined) { + if (inactiveCard?.cardID === undefined) { return; } - CardSettings.activatePhysicalExpensifyCard(lastFourDigits, physicalCard?.cardID); - }, [lastFourDigits, physicalCard?.cardID]); + CardSettings.activatePhysicalExpensifyCard(lastFourDigits, inactiveCard?.cardID); + }, [lastFourDigits, inactiveCard?.cardID]); - if (isEmptyObject(physicalCard)) { + if (isEmptyObject(inactiveCard)) { return <NotFoundPage />; } @@ -152,7 +152,7 @@ function ActivatePhysicalCardPage({ <Button success isDisabled={isOffline} - isLoading={physicalCard?.isLoading} + isLoading={inactiveCard?.isLoading} medium={isExtraSmallScreenHeight} large={!isExtraSmallScreenHeight} style={[styles.w100, styles.p5, styles.mtAuto]} diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 8c71746d32a3..1889fc556d0b 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -110,8 +110,8 @@ function BaseGetPhysicalCard({ const isRouteSet = useRef(false); const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; - const physicalCard = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); - const cardID = physicalCard?.cardID ?? 0; + const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); + const cardID = cardToBeIssued?.cardID ?? 0; useEffect(() => { if (isRouteSet.current || !privatePersonalDetails || !cardList) { @@ -119,15 +119,15 @@ function BaseGetPhysicalCard({ } // When there are no cards for the specified domain, user is redirected to the wallet page - if (domainCards.length === 0 || !physicalCard) { + if (domainCards.length === 0 || !cardToBeIssued) { Navigation.goBack(ROUTES.SETTINGS_WALLET); return; } // When there's no physical card or it exists but it doesn't have the required state for this flow, // redirect user to the espensify card page - if (physicalCard.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED) { - Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, physicalCard.cardID.toString())); + if (cardToBeIssued.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED) { + Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardToBeIssued.cardID.toString())); return; } @@ -142,7 +142,7 @@ function BaseGetPhysicalCard({ // Redirect user to previous steps of the flow if he hasn't finished them yet GetPhysicalCardUtils.setCurrentRoute(currentRoute, domain, GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues)); isRouteSet.current = true; - }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, physicalCard, privatePersonalDetails]); + }, [cardList, currentRoute, domain, domainCards.length, draftValues, loginList, cardToBeIssued, privatePersonalDetails]); const onSubmit = useCallback(() => { const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues); diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index af92d917754e..71305226add4 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -249,7 +249,7 @@ function ExpensifyCardPage({ titleStyle={styles.walletCardMenuItem} icon={Expensicons.Flag} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain, cardID))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain, String(card.cardID)))} /> </> ))} @@ -269,7 +269,7 @@ function ExpensifyCardPage({ title={translate('reportCardLostOrDamaged.report')} icon={Expensicons.Flag} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain, cardID))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain, String(card.cardID)))} /> </> ); @@ -282,7 +282,14 @@ function ExpensifyCardPage({ success large style={[styles.w100, styles.p5]} - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(domain, cardID))} + onPress={() => + Navigation.navigate( + ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute( + domain, + String(physicalCards?.find((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED)?.cardID), + ), + ) + } text={translate('activateCardPage.activatePhysicalCard')} /> )} diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 3edaa8282b56..f4800419f779 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -240,7 +240,8 @@ function PaymentMethodList({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing title: isAdminIssuedVirtualCard ? card?.nameValuePairs?.cardTitle || card.bank : card.bank, description: card.domainName, - onPress: () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName ?? '', card.cardID.toString() ?? '')), + onPress: () => + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName ?? '', isAdminIssuedVirtualCard ? card.cardID.toString() ?? '' : undefined)), isGroupedCardDomain: !isAdminIssuedVirtualCard, shouldShowRightIcon: true, interactive: true, @@ -253,7 +254,6 @@ function PaymentMethodList({ ...icon, }); }); - console.log('%%%%%\n', 'assignedCardsGrouped', assignedCardsGrouped); return assignedCardsGrouped; } @@ -298,7 +298,6 @@ function PaymentMethodList({ shouldShowRightIcon: true, }; }); - console.log('%%%%%\n', 'combinedPaymentMethods', combinedPaymentMethods); return combinedPaymentMethods; }, [shouldShowAssignedCards, fundList, bankAccountList, styles, filterType, isOffline, cardList, actionPaymentMethodType, activePaymentMethodID, StyleUtils, onPress]); From 15478c04f9872a3b0796a76ebea9c34b1571f743 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 22 Apr 2024 12:14:22 +0200 Subject: [PATCH 271/580] revert dev change --- src/libs/actions/Report.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 576c06d230c6..8b0dbf8a37a9 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3226,7 +3226,6 @@ function completeOnboarding( key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, value: { lastMentionedTime: DateUtils.getDBTime(), - permissions: [CONST.REPORT.PERMISSIONS.READ], }, }, { From a8454dcc79fee618247c52ddcc3215d58187f289 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Mon, 22 Apr 2024 12:43:20 +0200 Subject: [PATCH 272/580] fix button margins --- src/pages/settings/Wallet/ExpensifyCardPage.tsx | 6 +++--- src/pages/settings/Wallet/ReportCardLostPage.tsx | 1 + src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 71305226add4..901d66386701 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -90,7 +90,7 @@ function ExpensifyCardPage({ const [isNotFound, setIsNotFound] = useState(false); const cardsToShow = useMemo(() => { if (shouldDisplayCardDomain) { - return CardUtils.getDomainCards(cardList)[domain]?.filter((card) => !card?.nameValuePairs?.issuedBy); + return CardUtils.getDomainCards(cardList)[domain]?.filter((card) => !card?.nameValuePairs?.issuedBy) ?? []; } return [cardList?.[cardID]]; }, [shouldDisplayCardDomain, cardList, cardID, domain]); @@ -98,8 +98,8 @@ function ExpensifyCardPage({ setIsNotFound(!cardsToShow); }, [cardList, cardsToShow]); - const virtualCards = useMemo(() => cardsToShow.filter((card) => card?.nameValuePairs?.isVirtual), [cardsToShow]); - const physicalCards = useMemo(() => cardsToShow.filter((card) => !card?.nameValuePairs?.isVirtual), [cardsToShow]); + const virtualCards = useMemo(() => cardsToShow?.filter((card) => card?.nameValuePairs?.isVirtual), [cardsToShow]); + const physicalCards = useMemo(() => cardsToShow?.filter((card) => !card?.nameValuePairs?.isVirtual), [cardsToShow]); const [cardsDetails, setCardsDetails] = useState<Record<number, TCardDetails | null>>({}); const [isCardDetailsLoading, setIsCardDetailsLoading] = useState<Record<number, boolean>>({}); const [cardsDetailsErrors, setCardsDetailsErrors] = useState<Record<number, string>>({}); diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index 9c5fc84fc779..d914060cdd31 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -200,6 +200,7 @@ function ReportCardLostPage({ onSubmit={handleSubmitFirstStep} message="reportCardLostOrDamaged.reasonError" buttonText={translate('reportCardLostOrDamaged.nextButtonLabel')} + containerStyles={[styles.m5]} /> </> )} diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx index 9221e82a1d3e..22f8062183b5 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx @@ -76,6 +76,7 @@ function ReportVirtualCardFraudPage({ message={virtualCardError} isLoading={formData?.isLoading} buttonText={translate('reportFraudPage.deactivateCard')} + containerStyles={[styles.m5]} /> </View> </ScreenWrapper> From 9da12c42f15050a60b7b4cb15f28a97dd7f69d42 Mon Sep 17 00:00:00 2001 From: war-in <war-in@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:06:38 +0200 Subject: [PATCH 273/580] mvp --- package-lock.json | 6 ++--- package.json | 2 +- src/libs/ReportUtils.ts | 27 ++++++++++++++++---- src/libs/actions/Policy.ts | 2 +- src/libs/actions/Report.ts | 8 +++--- src/pages/workspace/WorkspaceNewRoomPage.tsx | 2 +- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8caa0401d28f..8cab4c43db4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", + "expensify-common": "git+ssh://git@github.com:software-mansion-labs/expensify-common.git#7596ae07c9cd5a6b5265897034898b4526d681dc", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -20209,8 +20209,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", - "integrity": "sha512-zz0/y0apISP1orxXEQOgn+Uod45O4wVypwwtaqcDPV4dH1tC3i4L98NoLSZvLn7Y17EcceSkfN6QCEsscgFTDQ==", + "resolved": "git+ssh://git@github.com/software-mansion-labs/expensify-common.git#7596ae07c9cd5a6b5265897034898b4526d681dc", + "integrity": "sha512-Ns7qkMuJ4SeLj0lrj3i+KqHBzjlym8baDlS7CUIqq2tuNXkgxwO4D+5d6U3ooLOf0CyWb56KaGy5TOTFqpJDZA==", "license": "MIT", "dependencies": { "classnames": "2.5.0", diff --git a/package.json b/package.json index 1461f0bfb77f..590bc1c9af79 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#c0f7f3b6558fbeda0527c80d68460d418afef219", + "expensify-common": "git+ssh://git@github.com:software-mansion-labs/expensify-common.git#7596ae07c9cd5a6b5265897034898b4526d681dc", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fc677dedc96e..0d0937c09bc3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3161,7 +3161,17 @@ function addDomainToShortMention(mention: string): string | undefined { * For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database * For longer comments, skip parsing, but still escape the text, and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! */ -function getParsedComment(text: string, shouldEscapeText?: boolean): string { +function getParsedComment(text: string, shouldEscapeText?: boolean, currentReportID?: string, policyID?: string): string { + let isGroupPolicyReport = false; + if (currentReportID) { + const currentReport = getReport(currentReportID); + isGroupPolicyReport = currentReport && !isEmptyObject(currentReport) ? isGroupPolicy(currentReport) : false; + } + if (policyID) { + const policyType = getPolicy(policyID).type; + isGroupPolicyReport = policyType === CONST.POLICY.TYPE.CORPORATE || policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.FREE; + } + const parser = new ExpensiMark(); const textWithMention = text.replace(CONST.REGEX.SHORT_MENTION, (match) => { const mention = match.substring(1); @@ -3169,7 +3179,7 @@ function getParsedComment(text: string, shouldEscapeText?: boolean): string { return mentionWithDomain ? `@${mentionWithDomain}` : match; }); - return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(textWithMention, {shouldEscapeText}) : lodashEscape(text); + return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(textWithMention, {shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']}) : lodashEscape(text); } function getReportDescriptionText(report: Report): string { @@ -3190,9 +3200,16 @@ function getPolicyDescriptionText(policy: OnyxEntry<Policy>): string { return parser.htmlToText(policy.description); } -function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number, createdOffset = 0, shouldEscapeText?: boolean): OptimisticReportAction { +function buildOptimisticAddCommentReportAction( + text?: string, + file?: FileObject, + actorAccountID?: number, + createdOffset = 0, + shouldEscapeText?: boolean, + reportID?: string, +): OptimisticReportAction { const parser = new ExpensiMark(); - const commentText = getParsedComment(text ?? '', shouldEscapeText); + const commentText = getParsedComment(text ?? '', shouldEscapeText, reportID); const isAttachmentOnly = file && !text; const isTextOnly = text && !file; @@ -3314,7 +3331,7 @@ function buildOptimisticTaskCommentReportAction( childOldestFourAccountIDs?: string; }, ): OptimisticReportAction { - const reportAction = buildOptimisticAddCommentReportAction(text, undefined, undefined, createdOffset); + const reportAction = buildOptimisticAddCommentReportAction(text, undefined, undefined, createdOffset, undefined, taskReportID); if (reportAction.reportAction.message?.[0]) { reportAction.reportAction.message[0].taskReportID = taskReportID; } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index cd375b580d85..f2ba65bf0717 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1722,7 +1722,7 @@ function updateWorkspaceDescription(policyID: string, description: string, curre if (description === currentDescription) { return; } - const parsedDescription = ReportUtils.getParsedComment(description); + const parsedDescription = ReportUtils.getParsedComment(description, undefined, undefined, policyID); const optimisticData: OnyxUpdate[] = [ { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 8b0dbf8a37a9..f062a4d78c68 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -446,7 +446,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { let commandName: typeof WRITE_COMMANDS.ADD_COMMENT | typeof WRITE_COMMANDS.ADD_ATTACHMENT | typeof WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT = WRITE_COMMANDS.ADD_COMMENT; if (text && !file) { - const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text); + const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text, undefined, undefined, undefined, undefined, reportID); reportCommentAction = reportComment.reportAction; reportCommentText = reportComment.commentText; } @@ -455,13 +455,13 @@ function addActions(reportID: string, text = '', file?: FileObject) { // When we are adding an attachment we will call AddAttachment. // It supports sending an attachment with an optional comment and AddComment supports adding a single text comment only. commandName = WRITE_COMMANDS.ADD_ATTACHMENT; - const attachment = ReportUtils.buildOptimisticAddCommentReportAction(text, file); + const attachment = ReportUtils.buildOptimisticAddCommentReportAction(text, file, undefined, undefined, undefined, reportID); attachmentAction = attachment.reportAction; } if (text && file) { // When there is both text and a file, the text for the report comment needs to be parsed) - reportCommentText = ReportUtils.getParsedComment(text ?? ''); + reportCommentText = ReportUtils.getParsedComment(text ?? '', undefined, reportID); // And the API command needs to go to the new API which supports combining both text and attachments in a single report action commandName = WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT; @@ -1848,7 +1848,7 @@ function updateDescription(reportID: string, previousValue: string, newValue: st return; } - const parsedDescription = ReportUtils.getParsedComment(newValue); + const parsedDescription = ReportUtils.getParsedComment(newValue, undefined, reportID); const optimisticData: OnyxUpdate[] = [ { diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 46a5b533372a..b2f9085502eb 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -105,7 +105,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli */ const submit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.NEW_ROOM_FORM>) => { const participants = [session?.accountID ?? 0]; - const parsedDescription = ReportUtils.getParsedComment(values.reportDescription ?? ''); + const parsedDescription = ReportUtils.getParsedComment(values.reportDescription ?? '', undefined, undefined, policyID); const policyReport = ReportUtils.buildOptimisticChatReport( participants, values.roomName, From 9db429655d129027e4a1defba2dc01cb95a5b307 Mon Sep 17 00:00:00 2001 From: war-in <war-in@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:16:47 +0200 Subject: [PATCH 274/580] update getCommentLength --- src/libs/ReportUtils.ts | 4 ++-- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- src/pages/home/report/ReportActionItemMessageEdit.tsx | 2 +- src/pages/workspace/WorkspaceNewRoomPage.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0d0937c09bc3..3df0194f6e5a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4931,8 +4931,8 @@ function getNewMarkerReportActionID(report: OnyxEntry<Report>, sortedAndFiltered * Used for compatibility with the backend auth validator for AddComment, and to account for MD in comments * @returns The comment's total length as seen from the backend */ -function getCommentLength(textComment: string): number { - return getParsedComment(textComment) +function getCommentLength(textComment: string, reportID?: string, policyID?: string): number { + return getParsedComment(textComment, undefined, reportID, policyID) .replace(/[^ -~]/g, '\\u????') .trim().length; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 8f42da5a1575..4be8db304b92 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -474,7 +474,7 @@ function ComposerWithSuggestions( const prepareCommentAndResetComposer = useCallback((): string => { const trimmedComment = commentRef.current.trim(); - const commentLength = ReportUtils.getCommentLength(trimmedComment); + const commentLength = ReportUtils.getCommentLength(trimmedComment, reportID); // Don't submit empty comments or comments that exceed the character limit if (!commentLength || commentLength > CONST.MAX_COMMENT_LENGTH) { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index fc3c92434fc4..79a22f61e17b 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -318,7 +318,7 @@ function ReportActionItemMessageEdit( */ const publishDraft = useCallback(() => { // Do nothing if draft exceed the character limit - if (ReportUtils.getCommentLength(draft) > CONST.MAX_COMMENT_LENGTH) { + if (ReportUtils.getCommentLength(draft, reportID) > CONST.MAX_COMMENT_LENGTH) { return; } diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index b2f9085502eb..ffd8cc478ef4 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -183,7 +183,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } - const descriptionLength = ReportUtils.getCommentLength(values.reportDescription); + const descriptionLength = ReportUtils.getCommentLength(values.reportDescription, policyID); if (descriptionLength > CONST.REPORT_DESCRIPTION.MAX_LENGTH) { ErrorUtils.addErrorMessage(errors, 'reportDescription', [ 'common.error.characterLimitExceedCounter', From d2059d7405513da455bda51e17e58d78e6128cc1 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Mon, 22 Apr 2024 18:48:10 +0700 Subject: [PATCH 275/580] allow edit payer when creating split bill request from global FAB --- .../MoneyRequestConfirmationList.tsx | 10 ++++----- .../step/IOURequestStepConfirmation.tsx | 22 ++++++++++++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 59d400735c0a..cbd62d7df6e7 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -690,15 +690,15 @@ function MoneyRequestConfirmationList({ <MenuItem key={translate('moneyRequestConfirmationList.paidBy')} label={translate('moneyRequestConfirmationList.paidBy')} - interactive={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate && !isReadOnly} - description={payeePersonalDetails.login} - title={payeePersonalDetails.displayName} + interactive={!isPolicyExpenseChat && !isReadOnly} + description={payeePersonalDetails.login ?? ReportUtils.getDisplayNameForParticipant(payeePersonalDetails.accountID)} + title={payeePersonalDetails.displayName ?? ReportUtils.getDisplayNameForParticipant(payeePersonalDetails.accountID)} icon={payeeIcons} onPress={() => { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); }} - shouldShowRightIcon={!isPolicyExpenseChat && !transaction?.isFromGlobalCreate && !isReadOnly} - titleWithTooltips={payeeTooltipDetails} + shouldShowRightIcon={!isPolicyExpenseChat && !isReadOnly} + titleWithTooltips={payeePersonalDetails?.isOptimisticPersonalDetail ? undefined : payeeTooltipDetails} /> ), shouldShow: isTypeSplit && action === CONST.IOU.ACTION.CREATE, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 4299893ac9c3..75c720c55e99 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -21,6 +21,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import * as UserUtils from '@libs/UserUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -77,7 +78,21 @@ function IOURequestStepConfirmation({ const transactionTaxAmount = transaction?.taxAmount; const isSharingTrackExpense = action === CONST.IOU.ACTION.SHARE; const isCategorizingTrackExpense = action === CONST.IOU.ACTION.CATEGORIZE; - const payeePersonalDetails = personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1]; + const payeePersonalDetails = useMemo(() => { + if (personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1]) { + return personalDetails?.[transaction?.splitPayerAccountIDs?.[0] ?? -1]; + } + + const participant = transaction?.participants?.find((val) => val.accountID === (transaction?.splitPayerAccountIDs?.[0] ?? -1)); + + return { + login: participant?.login ?? '', + accountID: participant?.accountID ?? -1, + avatar: UserUtils.getDefaultAvatarURL(participant?.accountID ?? -1), + displayName: participant?.login ?? '', + isOptimisticPersonalDetail: true, + }; + }, [personalDetails, transaction?.participants, transaction?.splitPayerAccountIDs]); const isRequestingFromTrackExpense = action === CONST.IOU.ACTION.REQUEST; const requestType = TransactionUtils.getRequestType(transaction); @@ -116,11 +131,11 @@ function IOURequestStepConfirmation({ const formHasBeenSubmitted = useRef(false); useEffect(() => { - if (transaction?.participants?.findIndex((participant) => participant.accountID === payeePersonalDetails?.accountID) !== -1 || iouType !== CONST.IOU.TYPE.SPLIT) { + if (transaction?.participants?.findIndex((participant) => participant.accountID === transaction?.splitPayerAccountIDs?.[0]) !== -1 || iouType !== CONST.IOU.TYPE.SPLIT) { return; } - const payeeParticipant: Participant = {accountID: payeePersonalDetails?.accountID, selected: true}; + const payeeParticipant: Participant = {accountID: transaction?.splitPayerAccountIDs?.[0], selected: true}; IOU.setMoneyRequestParticipants_temporaryForRefactor(transaction.transactionID, [payeeParticipant, ...(transaction?.participants ?? [])]); // We only want to run it when the component is mounted @@ -362,6 +377,7 @@ function IOURequestStepConfirmation({ tag: transaction.tag, billable: !!transaction.billable, iouRequestType: transaction.iouRequestType, + splitPayerAccoutIDs: transaction.splitPayerAccountIDs, }); } return; From ad960ff492088374a0454c117c9eb1e9d66e05da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Mon, 22 Apr 2024 14:05:47 +0200 Subject: [PATCH 276/580] Remove unused code --- src/ONYXKEYS.ts | 2 -- src/libs/actions/Report.ts | 12 ------------ 2 files changed, 14 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7c9247bcdbd7..622ab31a6300 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -336,7 +336,6 @@ const ONYXKEYS = { REPORT_ACTIONS_DRAFTS: 'reportActionsDrafts_', REPORT_ACTIONS_REACTIONS: 'reportActionsReactions_', REPORT_DRAFT_COMMENT: 'reportDraftComment_', - REPORT_DRAFT_COMMENT_NUMBER_OF_LINES: 'reportDraftCommentNumberOfLines_', REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_', REPORT_USER_IS_TYPING: 'reportUserIsTyping_', REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_', @@ -539,7 +538,6 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS]: OnyxTypes.ReportActionsDrafts; [ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS]: OnyxTypes.ReportActionReactions; [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT]: string; - [ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES]: number; [ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean; [ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: OnyxTypes.ReportUserIsTyping; [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 41079d20a982..4f6c010dabb8 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1109,11 +1109,6 @@ function saveReportDraftComment(reportID: string, comment: string | null) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, prepareDraftComment(comment)); } -/** Saves the number of lines for the comment */ -function saveReportCommentNumberOfLines(reportID: string, numberOfLines: number) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, numberOfLines); -} - /** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */ function broadcastUserIsTyping(reportID: string) { const privateReportChannelName = getReportChannelName(reportID); @@ -1440,11 +1435,6 @@ function saveReportActionDraft(reportID: string, reportAction: ReportAction, dra Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`, {[reportAction.reportActionID]: {message: draftMessage}}); } -/** Saves the number of lines for the report action draft */ -function saveReportActionDraftNumberOfLines(reportID: string, reportActionID: string, numberOfLines: number) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}_${reportActionID}`, numberOfLines); -} - function updateNotificationPreference( reportID: string, previousValue: NotificationPreference | undefined, @@ -3263,7 +3253,6 @@ export { unsubscribeFromReportChannel, unsubscribeFromLeavingRoomReportChannel, saveReportDraftComment, - saveReportCommentNumberOfLines, broadcastUserIsTyping, broadcastUserIsLeavingRoom, togglePinnedState, @@ -3271,7 +3260,6 @@ export { handleUserDeletedLinksInHtml, deleteReportActionDraft, saveReportActionDraft, - saveReportActionDraftNumberOfLines, deleteReportComment, navigateToConciergeChat, addPolicyReport, From 909b5a8f7f9df4427b2792bfd95d718ddabbf9be Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 22 Apr 2024 14:30:02 +0200 Subject: [PATCH 277/580] extract createProxyForValue and further split up utils --- src/libs/actions/OnyxUpdateManager/index.ts | 16 +++----- .../OnyxUpdateManager/utils/applyUpdates.ts | 9 +++++ .../utils/deferredUpdates.ts | 8 ++++ .../{utils.ts => utils/index.ts} | 37 +++++++++++-------- src/utils/createProxyForValue.ts | 19 ++++++++++ 5 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts create mode 100644 src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts rename src/libs/actions/OnyxUpdateManager/{utils.ts => utils/index.ts} (82%) create mode 100644 src/utils/createProxyForValue.ts diff --git a/src/libs/actions/OnyxUpdateManager/index.ts b/src/libs/actions/OnyxUpdateManager/index.ts index 176fd10899fb..0fc84222d414 100644 --- a/src/libs/actions/OnyxUpdateManager/index.ts +++ b/src/libs/actions/OnyxUpdateManager/index.ts @@ -9,6 +9,7 @@ import type {OnyxUpdatesFromServer, Response} from '@src/types/onyx'; import {isValidOnyxUpdateFromServer} from '@src/types/onyx/OnyxUpdatesFromServer'; import type DeferredUpdatesDictionary from './types'; import * as OnyxUpdateManagerUtils from './utils'; +import deferredUpdatesProxy from './utils/deferredUpdates'; // This file is in charge of looking at the updateIDs coming from the server and comparing them to the last updateID that the client has. // If the client is behind the server, then we need to @@ -42,12 +43,9 @@ Onyx.connect({ // eslint-disable-next-line import/no-mutable-exports let queryPromise: Promise<Response | Response[] | void> | undefined; -// eslint-disable-next-line import/no-mutable-exports -let deferredUpdates: DeferredUpdatesDictionary = {}; - const resetDeferralLogicVariables = () => { queryPromise = undefined; - deferredUpdates = {}; + deferredUpdatesProxy.deferredUpdates = {}; }; // This function will reset the query variables, unpause the SequentialQueue and log an info to the user. @@ -115,10 +113,10 @@ function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromSer // The flow below is setting the promise to a getMissingOnyxUpdates to address flow (2) explained above. // Get the number of deferred updates before adding the new one - const existingDeferredUpdatesCount = Object.keys(deferredUpdates).length; + const existingDeferredUpdatesCount = Object.keys(deferredUpdatesProxy.deferredUpdates).length; // Add the new update to the deferred updates - deferredUpdates[Number(updateParams.lastUpdateID)] = updateParams; + deferredUpdatesProxy.deferredUpdates[Number(updateParams.lastUpdateID)] = updateParams; // If there are deferred updates already, we don't need to fetch the missing updates again. if (existingDeferredUpdatesCount > 0) { @@ -134,9 +132,7 @@ function handleOnyxUpdateGap(onyxUpdatesFromServer: OnyxEntry<OnyxUpdatesFromSer // Get the missing Onyx updates from the server and afterwards validate and apply the deferred updates. // This will trigger recursive calls to "validateAndApplyDeferredUpdates" if there are gaps in the deferred updates. - queryPromise = App.getMissingOnyxUpdates(lastUpdateIDFromClient, previousUpdateIDFromServer) - .then(() => OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates(deferredUpdates, lastUpdateIDFromClient)) - .then((newDeferredUpdates) => (deferredUpdates = newDeferredUpdates)); + queryPromise = App.getMissingOnyxUpdates(lastUpdateIDFromClient, previousUpdateIDFromServer).then(() => OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates(clientLastUpdateID)); } queryPromise.finally(finalizeUpdatesAndResumeQueue); @@ -151,5 +147,5 @@ export default () => { }; export {handleOnyxUpdateGap}; -export {queryPromise, deferredUpdates, resetDeferralLogicVariables}; +export {queryPromise, resetDeferralLogicVariables}; export type {DeferredUpdatesDictionary}; diff --git a/src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts new file mode 100644 index 000000000000..37a1ec70728d --- /dev/null +++ b/src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts @@ -0,0 +1,9 @@ +// We need to keep this in a separate file, so that we can mock this function in tests. +import type DeferredUpdatesDictionary from '@libs/actions/OnyxUpdateManager/types'; +import * as OnyxUpdates from '@userActions/OnyxUpdates'; + +// This function applies a list of updates to Onyx in order and resolves when all updates have been applied +const applyUpdates = (updates: DeferredUpdatesDictionary) => Promise.all(Object.values(updates).map((update) => OnyxUpdates.apply(update))); + +// eslint-disable-next-line import/prefer-default-export +export {applyUpdates}; diff --git a/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts new file mode 100644 index 000000000000..4463e8ea5a7b --- /dev/null +++ b/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts @@ -0,0 +1,8 @@ +import type DeferredUpdatesDictionary from '@libs/actions/OnyxUpdateManager/types'; +import createProxyForValue from '@src/utils/createProxyForValue'; + +const deferredUpdatesValue = {deferredUpdates: {} as DeferredUpdatesDictionary}; + +const deferredUpdatesProxy = createProxyForValue(deferredUpdatesValue, 'deferredUpdates'); + +export default deferredUpdatesProxy; diff --git a/src/libs/actions/OnyxUpdateManager/utils.ts b/src/libs/actions/OnyxUpdateManager/utils/index.ts similarity index 82% rename from src/libs/actions/OnyxUpdateManager/utils.ts rename to src/libs/actions/OnyxUpdateManager/utils/index.ts index bdbcfa13e37a..42451fae2a28 100644 --- a/src/libs/actions/OnyxUpdateManager/utils.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/index.ts @@ -1,9 +1,15 @@ +import Onyx from 'react-native-onyx'; import * as App from '@userActions/App'; -import * as OnyxUpdates from '@userActions/OnyxUpdates'; -import type DeferredUpdatesDictionary from './types'; +import type DeferredUpdatesDictionary from '@userActions/OnyxUpdateManager/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {applyUpdates} from './applyUpdates'; +import deferredUpdatesProxy from './deferredUpdates'; -// This function applies a list of updates to Onyx in order and resolves when all updates have been applied -const applyUpdates = (updates: DeferredUpdatesDictionary) => Promise.all(Object.values(updates).map((update) => OnyxUpdates.apply(update))); +let lastUpdateIDAppliedToClient = 0; +Onyx.connect({ + key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, + callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), +}); // In order for the deferred updates to be applied correctly in order, // we need to check if there are any gaps between deferred updates. @@ -70,12 +76,12 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary, lastUpdateIDFrom // This function will check for gaps in the deferred updates and // apply the updates in order after the missing updates are fetched and applied -function validateAndApplyDeferredUpdates(deferredUpdates: DeferredUpdatesDictionary, lastUpdateIDFromClient: number): Promise<DeferredUpdatesDictionary> { - let newDeferredUpdates = {...deferredUpdates}; +function validateAndApplyDeferredUpdates(clientLastUpdateID?: number): Promise<void> { + const lastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0; // We only want to apply deferred updates that are newer than the last update that was applied to the client. // At this point, the missing updates from "GetMissingOnyxUpdates" have been applied already, so we can safely filter out. - const pendingDeferredUpdates = Object.entries(newDeferredUpdates).reduce<DeferredUpdatesDictionary>( + const pendingDeferredUpdates = Object.entries(deferredUpdatesProxy.deferredUpdates).reduce<DeferredUpdatesDictionary>( (accUpdates, [lastUpdateID, update]) => ({ ...accUpdates, ...(Number(lastUpdateID) > lastUpdateIDFromClient ? {[Number(lastUpdateID)]: update} : {}), @@ -86,7 +92,7 @@ function validateAndApplyDeferredUpdates(deferredUpdates: DeferredUpdatesDiction // If there are no remaining deferred updates after filtering out outdated ones, // we can just unpause the queue and return if (Object.values(pendingDeferredUpdates).length === 0) { - return Promise.resolve(newDeferredUpdates); + return Promise.resolve(); } const {applicableUpdates, updatesAfterGaps, latestMissingUpdateID} = detectGapsAndSplit(pendingDeferredUpdates, lastUpdateIDFromClient); @@ -95,35 +101,36 @@ function validateAndApplyDeferredUpdates(deferredUpdates: DeferredUpdatesDiction // re-fetch the missing updates and then apply the remaining deferred updates after the gap if (latestMissingUpdateID) { return new Promise((resolve, reject) => { - newDeferredUpdates = {}; + deferredUpdatesProxy.deferredUpdates = {}; applyUpdates(applicableUpdates).then(() => { // After we have applied the applicable updates, there might have been new deferred updates added. // In the next (recursive) call of "validateAndApplyDeferredUpdates", // the initial "updatesAfterGaps" and all new deferred updates will be applied in order, // as long as there was no new gap detected. Otherwise repeat the process. - newDeferredUpdates = {...newDeferredUpdates, ...updatesAfterGaps}; + deferredUpdatesProxy.deferredUpdates = {...deferredUpdatesProxy.deferredUpdates, ...updatesAfterGaps}; + // updateDeferredUpdates(newDeferredUpdates); // It should not be possible for lastUpdateIDAppliedToClient to be null, therefore we can ignore this case. // If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates. if (latestMissingUpdateID <= lastUpdateIDFromClient) { - validateAndApplyDeferredUpdates(newDeferredUpdates, lastUpdateIDFromClient) - .then(() => resolve(newDeferredUpdates)) + validateAndApplyDeferredUpdates(clientLastUpdateID) + .then(() => resolve(undefined)) .catch(reject); return; } // Then we can fetch the missing updates and apply them App.getMissingOnyxUpdates(lastUpdateIDFromClient, latestMissingUpdateID) - .then(() => validateAndApplyDeferredUpdates(newDeferredUpdates, lastUpdateIDFromClient)) - .then(() => resolve(newDeferredUpdates)) + .then(() => validateAndApplyDeferredUpdates(clientLastUpdateID)) + .then(() => resolve(undefined)) .catch(reject); }); }); } // If there are no gaps in the deferred updates, we can apply all deferred updates in order - return applyUpdates(applicableUpdates).then(() => newDeferredUpdates); + return applyUpdates(applicableUpdates).then(() => undefined); } export {applyUpdates, detectGapsAndSplit, validateAndApplyDeferredUpdates}; diff --git a/src/utils/createProxyForValue.ts b/src/utils/createProxyForValue.ts new file mode 100644 index 000000000000..e9e8eb5dca6b --- /dev/null +++ b/src/utils/createProxyForValue.ts @@ -0,0 +1,19 @@ +const createProxyForValue = <Value, VariableName extends string>(value: Record<VariableName, Value>, variableName: VariableName) => + new Proxy(value, { + get: (target, prop) => { + if (prop !== variableName) { + return undefined; + } + return target[prop as VariableName]; + }, + set: (target, prop, newValue) => { + if (prop !== variableName) { + return false; + } + // eslint-disable-next-line no-param-reassign + target[prop as VariableName] = newValue; + return true; + }, + }); + +export default createProxyForValue; From c502513afd4699d29bc6c6103a5656662f0bae94 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 22 Apr 2024 14:30:40 +0200 Subject: [PATCH 278/580] fix: tests --- tests/unit/OnyxUpdateManagerTest.ts | 129 +++++++++++----------------- 1 file changed, 50 insertions(+), 79 deletions(-) diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 4d7684a66251..f96d339c8df2 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -1,13 +1,13 @@ import Onyx from 'react-native-onyx'; import * as AppImport from '@libs/actions/App'; -import * as OnyxUpdateManagerImport from '@libs/actions/OnyxUpdateManager'; +import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager'; +import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; +import deferredUpdatesProxy from '@libs/actions/OnyxUpdateManager/utils/deferredUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -const lastAppliedMissingUpdateIDOnyxKey = 'lastAppliedMissingUpdateID'; - const createTriggerPromise = () => { let trigger: () => void = () => undefined; const resetPromise = () => @@ -20,35 +20,40 @@ const createTriggerPromise = () => { }; type AppActionsMock = typeof AppImport & { + lastUpdateIdFromUpdatesProxy: Record<'lastUpdateIdFromUpdates', number>; getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; getMissingOnyxUpdatesTriggeredPromise: Promise<void>; + getMissingOnyxUpdatesDonePromise: Promise<void>; }; jest.mock('@libs/actions/App', () => { const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); const AppOnyx: typeof Onyx = jest.requireActual('react-native-onyx').default; const APP_ONYXKEYS: typeof ONYXKEYS = jest.requireActual('@src/ONYXKEYS').default; + const appCreateProxyForValue = jest.requireActual('@src/utils/createProxyForValue').default; - let appLastAppliedMissingUpdateID = 2; - AppOnyx.connect({ - // @ts-expect-error ignore invalid onyx key - key: 'lastAppliedMissingUpdateID', - callback: (value) => (appLastAppliedMissingUpdateID = (value as number | null) ?? 2), - }); + const lastUpdateIdFromUpdatesValueInternal = {lastUpdateIdFromUpdates: 2}; + const lastUpdateIdFromUpdatesProxy = appCreateProxyForValue(lastUpdateIdFromUpdatesValueInternal, 'lastUpdateIdFromUpdates'); + const {lastUpdateIdFromUpdates} = lastUpdateIdFromUpdatesValueInternal; - const {promise: getMissingOnyxUpdatesTriggeredPromise, trigger: getMissingOnyxUpdatesWasTriggered, resetPromise: resetGetMissingOnyxUpdatesPromise} = createTriggerPromise(); + const {promise: getMissingOnyxUpdatesTriggeredPromise, trigger: getMissingOnyxUpdatesWasTriggered, resetPromise: resetGetMissingOnyxUpdatesTriggeredPromise} = createTriggerPromise(); + const {promise: getMissingOnyxUpdatesDonePromise, trigger: getMissingOnyxUpdatesDone, resetPromise: resetGetMissingOnyxUpdatesDonePromise} = createTriggerPromise(); return { ...AppImplementation, + lastUpdateIdFromUpdatesProxy, finalReconnectAppAfterActivatingReliableUpdates: jest.fn(() => Promise.resolve()), getMissingOnyxUpdatesTriggeredPromise, + getMissingOnyxUpdatesDonePromise, getMissingOnyxUpdates: jest.fn(() => { + resetGetMissingOnyxUpdatesDonePromise(); getMissingOnyxUpdatesWasTriggered(); - const promise = AppOnyx.set(APP_ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, appLastAppliedMissingUpdateID); + const promise = AppOnyx.set(APP_ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIdFromUpdates); promise.finally(() => { - resetGetMissingOnyxUpdatesPromise(); + getMissingOnyxUpdatesDone(); + resetGetMissingOnyxUpdatesTriggeredPromise(); }); return promise; @@ -56,60 +61,38 @@ jest.mock('@libs/actions/App', () => { } as AppActionsMock; }); -type OnyxUpdateManagerMock = typeof OnyxUpdateManagerImport & { - applyUpdates: jest.Mock<Promise<Response[]>>; +type ApplyUpdatesMock = typeof ApplyUpdatesImport & { + applyUpdates: jest.Mock<Promise<void[]>>; applyUpdatesTriggeredPromise: Promise<void>; }; -jest.mock('@libs/actions/OnyxUpdateManager', () => { - const OnyxUpdateManagerImplementation: typeof OnyxUpdateManagerImport = jest.requireActual('@libs/actions/OnyxUpdateManager'); +jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates', () => { + const AppOnyx: typeof Onyx = jest.requireActual('react-native-onyx').default; + const APP_ONYXKEYS: typeof ONYXKEYS = jest.requireActual('@src/ONYXKEYS').default; const {promise: applyUpdatesTriggeredPromise, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggeredPromise} = createTriggerPromise(); - // return { - // ...OnyxUpdateManagerImplementation, - // applyUpdatesTriggeredPromise, - // applyUpdates: jest.fn((updates: DeferredUpdatesDictionary) => { - // applyUpdatesTriggered(); - - // const promise = OnyxUpdateManagerImplementation.applyUpdates(updates); - - // promise.finally(() => { - // resetApplyUpdatesTriggeredPromise(); - // }); - - // return promise; - // }), - // } as OnyxUpdateManagerMock; - - return new Proxy(OnyxUpdateManagerImplementation, { - get: (target, prop) => { - switch (prop) { - case 'applyUpdatesTriggeredPromise': - return applyUpdatesTriggeredPromise; - case 'applyUpdates': - return jest.fn((updates: DeferredUpdatesDictionary) => { - applyUpdatesTriggered(); + return { + applyUpdatesTriggeredPromise, + applyUpdates: jest.fn((updates: DeferredUpdatesDictionary) => { + applyUpdatesTriggered(); - console.log('apply updates triggered'); + const lastUpdateIdFromUpdates = Math.max(...Object.keys(updates).map(Number)); - const promise = OnyxUpdateManagerImplementation.applyUpdates(updates); + const promise = AppOnyx.set(APP_ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIdFromUpdates); - promise.finally(() => { - resetApplyUpdatesTriggeredPromise(); - }); + promise.finally(() => { + resetApplyUpdatesTriggeredPromise(); + }); - return promise; - }); - default: - return target[prop as keyof typeof OnyxUpdateManagerImport]; - } - }, - }) as OnyxUpdateManagerMock; + return promise; + }), + } as ApplyUpdatesMock; }); const App = AppImport as AppActionsMock; -const OnyxUpdateManager = OnyxUpdateManagerImport as OnyxUpdateManagerMock; +const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; +const {applyUpdates} = ApplyUpdates; const createMockUpdate = (lastUpdateID: number): OnyxUpdatesFromServer => ({ type: 'https', @@ -139,59 +122,47 @@ const mockUpdate5 = createMockUpdate(5); const mockUpdate6 = createMockUpdate(6); const resetOnyxUpdateManager = async () => { - // @ts-expect-error ignore invalid onyx key - await Onyx.set(lastAppliedMissingUpdateIDOnyxKey, 2); + App.lastUpdateIdFromUpdatesProxy.lastUpdateIdFromUpdates = 2; await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); OnyxUpdateManager.resetDeferralLogicVariables(); }; describe('OnyxUpdateManager', () => { - beforeAll(() => { - Onyx.init({keys: ONYXKEYS}); - return waitForBatchedUpdates(); - }); + let lastUpdateIDAppliedToClient = 1; beforeEach(async () => { jest.clearAllMocks(); Onyx.clear(); await resetOnyxUpdateManager(); - return waitForBatchedUpdates(); - }); - - it('should fetch missing Onyx updates once, defer updates and apply after missing updates', () => { - let lastUpdateIDAppliedToClient = 0; + Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), + callback: (value) => (lastUpdateIDAppliedToClient = value ?? 1), }); + return waitForBatchedUpdates(); + }); + it('should fetch missing Onyx updates once, defer updates and apply after missing updates', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); return App.getMissingOnyxUpdatesTriggeredPromise .then(() => { - expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); + expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); }) .then(waitForBatchedUpdates) .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { expect(lastUpdateIDAppliedToClient).toBe(5); - expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); + expect(applyUpdates).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/naming-convention - expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); + expect(applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); }); }); it('should only apply deferred updates that are after the locally applied update', async () => { - let lastUpdateIDAppliedToClient = 0; - Onyx.connect({ - key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), - }); - - // @ts-expect-error ignore invalid onyx key - await Onyx.set(lastAppliedMissingUpdateIDOnyxKey, 3); + App.lastUpdateIdFromUpdatesProxy.lastUpdateIdFromUpdates = 3; OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); @@ -199,16 +170,16 @@ describe('OnyxUpdateManager', () => { return App.getMissingOnyxUpdatesTriggeredPromise .then(() => { - expect(Object.keys(OnyxUpdateManager.deferredUpdates)).toHaveLength(3); + expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); }) .then(waitForBatchedUpdates) .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { expect(lastUpdateIDAppliedToClient).toBe(6); - expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledTimes(1); + expect(applyUpdates).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/naming-convention - expect(OnyxUpdateManager.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); + expect(applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); }); }); }); From 3e448b6425e8a7de5f0d3bcfaeeaffcdc438b21b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Mon, 22 Apr 2024 14:41:29 +0200 Subject: [PATCH 279/580] use proper type --- src/types/onyx/Report.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 05aa6bc153b9..ead526bba987 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -30,11 +30,11 @@ type Participant = { type InvoiceReceiver = | { - type: 'individual'; + type: typeof CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; accountID: number; } | { - type: 'policy'; + type: typeof CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS; policyID: string; }; From 001eb3aa4fe0a53df03a69b1664b28cc8901b396 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 22 Apr 2024 15:25:03 +0200 Subject: [PATCH 280/580] extract mocks from test --- .../utils/__mocks__/applyUpdates.ts | 32 ++++ .../actions/OnyxUpdateManager/utils/index.ts | 4 + src/libs/actions/__mocks__/App.ts | 74 +++++++++ src/utils/createTriggerPromise.ts | 12 ++ tests/unit/OnyxUpdateManagerTest.ts | 150 ++++++++---------- 5 files changed, 191 insertions(+), 81 deletions(-) create mode 100644 src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts create mode 100644 src/libs/actions/__mocks__/App.ts create mode 100644 src/utils/createTriggerPromise.ts diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts new file mode 100644 index 000000000000..a5a916cadfe4 --- /dev/null +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts @@ -0,0 +1,32 @@ +import Onyx from 'react-native-onyx'; +import type DeferredUpdatesDictionary from '@libs/actions/OnyxUpdateManager/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import createTriggerPromise from '@src/utils/createTriggerPromise'; + +const {promise: applyUpdatesTriggeredPromise, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggeredPromise} = createTriggerPromise(); + +const applyUpdates = jest.fn((updates: DeferredUpdatesDictionary) => { + console.log('apply updates'); + + applyUpdatesTriggered(); + + const lastUpdateIdFromUpdates = Math.max(...Object.keys(updates).map(Number)); + + const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIdFromUpdates); + + promise.finally(() => { + resetApplyUpdatesTriggeredPromise(); + }); + + promise + .then(() => { + console.log('applyUpdates succeeded'); + }) + .catch((e) => { + console.log('applyUpdates failed', e); + }); + + return promise; +}); + +export {applyUpdates, applyUpdatesTriggeredPromise}; diff --git a/src/libs/actions/OnyxUpdateManager/utils/index.ts b/src/libs/actions/OnyxUpdateManager/utils/index.ts index 42451fae2a28..0ca42a46db7a 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/index.ts @@ -103,11 +103,15 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number): Promise<v return new Promise((resolve, reject) => { deferredUpdatesProxy.deferredUpdates = {}; + console.log({applicableUpdates}); + applyUpdates(applicableUpdates).then(() => { // After we have applied the applicable updates, there might have been new deferred updates added. // In the next (recursive) call of "validateAndApplyDeferredUpdates", // the initial "updatesAfterGaps" and all new deferred updates will be applied in order, // as long as there was no new gap detected. Otherwise repeat the process. + + console.log({updatesAfterGaps}); deferredUpdatesProxy.deferredUpdates = {...deferredUpdatesProxy.deferredUpdates, ...updatesAfterGaps}; // updateDeferredUpdates(newDeferredUpdates); diff --git a/src/libs/actions/__mocks__/App.ts b/src/libs/actions/__mocks__/App.ts new file mode 100644 index 000000000000..81f3cbec41f3 --- /dev/null +++ b/src/libs/actions/__mocks__/App.ts @@ -0,0 +1,74 @@ +import Onyx from 'react-native-onyx'; +import type * as AppImport from '@libs/actions/App'; +import ONYXKEYS from '@src/ONYXKEYS'; +import createProxyForValue from '@src/utils/createProxyForValue'; +import createTriggerPromise from '@src/utils/createTriggerPromise'; + +const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); +const { + setLocale, + setLocaleAndNavigate, + setSidebarLoaded, + setUpPoliciesAndNavigate, + openProfile, + redirectThirdPartyDesktopSignIn, + openApp, + reconnectApp, + confirmReadyToOpenApp, + handleRestrictedEvent, + beginDeepLinkRedirect, + beginDeepLinkRedirectAfterTransition, + finalReconnectAppAfterActivatingReliableUpdates, + savePolicyDraftByNewWorkspace, + createWorkspaceWithPolicyDraftAndNavigateToIt, + updateLastVisitedPath, + KEYS_TO_PRESERVE, +} = AppImplementation; + +const shouldGetMissingOnyxUpdatesUpToIdValue = {shouldGetMissingOnyxUpdatesUpToId: 2}; +const shouldGetMissingOnyxUpdatesUpToIdProxy = createProxyForValue(shouldGetMissingOnyxUpdatesUpToIdValue, 'shouldGetMissingOnyxUpdatesUpToId'); +const {shouldGetMissingOnyxUpdatesUpToId} = shouldGetMissingOnyxUpdatesUpToIdValue; + +const {promise: getMissingOnyxUpdatesTriggeredPromise, trigger: getMissingOnyxUpdatesWasTriggered, resetPromise: resetGetMissingOnyxUpdatesTriggeredPromise} = createTriggerPromise(); +const {promise: getMissingOnyxUpdatesDonePromise, trigger: getMissingOnyxUpdatesDone, resetPromise: resetGetMissingOnyxUpdatesDonePromise} = createTriggerPromise(); + +const getMissingOnyxUpdates = jest.fn(() => { + resetGetMissingOnyxUpdatesDonePromise(); + getMissingOnyxUpdatesWasTriggered(); + + const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, shouldGetMissingOnyxUpdatesUpToId); + + promise.finally(() => { + getMissingOnyxUpdatesDone(); + resetGetMissingOnyxUpdatesTriggeredPromise(); + }); + + return promise; +}); + +export { + // Mocks + shouldGetMissingOnyxUpdatesUpToIdProxy, + getMissingOnyxUpdatesTriggeredPromise, + getMissingOnyxUpdatesDonePromise, + getMissingOnyxUpdates, + + // Actual App implementation + setLocale, + setLocaleAndNavigate, + setSidebarLoaded, + setUpPoliciesAndNavigate, + openProfile, + redirectThirdPartyDesktopSignIn, + openApp, + reconnectApp, + confirmReadyToOpenApp, + handleRestrictedEvent, + beginDeepLinkRedirect, + beginDeepLinkRedirectAfterTransition, + finalReconnectAppAfterActivatingReliableUpdates, + savePolicyDraftByNewWorkspace, + createWorkspaceWithPolicyDraftAndNavigateToIt, + updateLastVisitedPath, + KEYS_TO_PRESERVE, +}; diff --git a/src/utils/createTriggerPromise.ts b/src/utils/createTriggerPromise.ts new file mode 100644 index 000000000000..9060ba22ec86 --- /dev/null +++ b/src/utils/createTriggerPromise.ts @@ -0,0 +1,12 @@ +const createTriggerPromise = () => { + let trigger: () => void = () => undefined; + const resetPromise = () => + new Promise<void>((resolve) => { + trigger = resolve; + }); + const promise = resetPromise(); + + return {promise, trigger, resetPromise}; +}; + +export default createTriggerPromise; diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index f96d339c8df2..996891f725c6 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -1,98 +1,29 @@ import Onyx from 'react-native-onyx'; import * as AppImport from '@libs/actions/App'; import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; -import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager'; import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import deferredUpdatesProxy from '@libs/actions/OnyxUpdateManager/utils/deferredUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -const createTriggerPromise = () => { - let trigger: () => void = () => undefined; - const resetPromise = () => - new Promise<void>((resolve) => { - trigger = resolve; - }); - const promise = resetPromise(); - - return {promise, trigger, resetPromise}; -}; +jest.mock('@libs/actions/App'); +jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); type AppActionsMock = typeof AppImport & { - lastUpdateIdFromUpdatesProxy: Record<'lastUpdateIdFromUpdates', number>; + shouldGetMissingOnyxUpdatesUpToIdProxy: Record<'shouldGetMissingOnyxUpdatesUpToId', number>; getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; getMissingOnyxUpdatesTriggeredPromise: Promise<void>; getMissingOnyxUpdatesDonePromise: Promise<void>; }; -jest.mock('@libs/actions/App', () => { - const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); - const AppOnyx: typeof Onyx = jest.requireActual('react-native-onyx').default; - const APP_ONYXKEYS: typeof ONYXKEYS = jest.requireActual('@src/ONYXKEYS').default; - const appCreateProxyForValue = jest.requireActual('@src/utils/createProxyForValue').default; - - const lastUpdateIdFromUpdatesValueInternal = {lastUpdateIdFromUpdates: 2}; - const lastUpdateIdFromUpdatesProxy = appCreateProxyForValue(lastUpdateIdFromUpdatesValueInternal, 'lastUpdateIdFromUpdates'); - const {lastUpdateIdFromUpdates} = lastUpdateIdFromUpdatesValueInternal; - - const {promise: getMissingOnyxUpdatesTriggeredPromise, trigger: getMissingOnyxUpdatesWasTriggered, resetPromise: resetGetMissingOnyxUpdatesTriggeredPromise} = createTriggerPromise(); - const {promise: getMissingOnyxUpdatesDonePromise, trigger: getMissingOnyxUpdatesDone, resetPromise: resetGetMissingOnyxUpdatesDonePromise} = createTriggerPromise(); - - return { - ...AppImplementation, - lastUpdateIdFromUpdatesProxy, - finalReconnectAppAfterActivatingReliableUpdates: jest.fn(() => Promise.resolve()), - getMissingOnyxUpdatesTriggeredPromise, - getMissingOnyxUpdatesDonePromise, - getMissingOnyxUpdates: jest.fn(() => { - resetGetMissingOnyxUpdatesDonePromise(); - getMissingOnyxUpdatesWasTriggered(); - - const promise = AppOnyx.set(APP_ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIdFromUpdates); - - promise.finally(() => { - getMissingOnyxUpdatesDone(); - resetGetMissingOnyxUpdatesTriggeredPromise(); - }); - - return promise; - }), - } as AppActionsMock; -}); - type ApplyUpdatesMock = typeof ApplyUpdatesImport & { applyUpdates: jest.Mock<Promise<void[]>>; applyUpdatesTriggeredPromise: Promise<void>; }; -jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates', () => { - const AppOnyx: typeof Onyx = jest.requireActual('react-native-onyx').default; - const APP_ONYXKEYS: typeof ONYXKEYS = jest.requireActual('@src/ONYXKEYS').default; - - const {promise: applyUpdatesTriggeredPromise, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggeredPromise} = createTriggerPromise(); - - return { - applyUpdatesTriggeredPromise, - applyUpdates: jest.fn((updates: DeferredUpdatesDictionary) => { - applyUpdatesTriggered(); - - const lastUpdateIdFromUpdates = Math.max(...Object.keys(updates).map(Number)); - - const promise = AppOnyx.set(APP_ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIdFromUpdates); - - promise.finally(() => { - resetApplyUpdatesTriggeredPromise(); - }); - - return promise; - }), - } as ApplyUpdatesMock; -}); - const App = AppImport as AppActionsMock; const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; -const {applyUpdates} = ApplyUpdates; const createMockUpdate = (lastUpdateID: number): OnyxUpdatesFromServer => ({ type: 'https', @@ -122,7 +53,7 @@ const mockUpdate5 = createMockUpdate(5); const mockUpdate6 = createMockUpdate(6); const resetOnyxUpdateManager = async () => { - App.lastUpdateIdFromUpdatesProxy.lastUpdateIdFromUpdates = 2; + App.shouldGetMissingOnyxUpdatesUpToIdProxy.shouldGetMissingOnyxUpdatesUpToId = 2; await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); OnyxUpdateManager.resetDeferralLogicVariables(); }; @@ -134,7 +65,6 @@ describe('OnyxUpdateManager', () => { jest.clearAllMocks(); Onyx.clear(); await resetOnyxUpdateManager(); - Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, callback: (value) => (lastUpdateIDAppliedToClient = value ?? 1), @@ -149,20 +79,27 @@ describe('OnyxUpdateManager', () => { return App.getMissingOnyxUpdatesTriggeredPromise .then(() => { + // Missing updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) should have been fetched from the server + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); + // All scheduled updates should have been added to the deferred list expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); }) .then(waitForBatchedUpdates) .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. expect(lastUpdateIDAppliedToClient).toBe(5); - expect(applyUpdates).toHaveBeenCalledTimes(1); + + // There should be only one call to applyUpdates. The call should contain all the deferred update, + // since the locally applied updates have changed in the meantime. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/naming-convention - expect(applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); }); }); - it('should only apply deferred updates that are after the locally applied update', async () => { - App.lastUpdateIdFromUpdatesProxy.lastUpdateIdFromUpdates = 3; + it('should only apply deferred updates that are after the locally applied update (pending updates)', async () => { + App.shouldGetMissingOnyxUpdatesUpToIdProxy.shouldGetMissingOnyxUpdatesUpToId = 3; OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); @@ -170,16 +107,67 @@ describe('OnyxUpdateManager', () => { return App.getMissingOnyxUpdatesTriggeredPromise .then(() => { + // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); + // All scheduled updates should have been added to the deferred list expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); - Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); + + // We manually update the lastUpdateIDAppliedToClient to 5, to simulate local updates being applied, + // while we are waiting for the missing updates to be fetched. + // Only the deferred updates after the lastUpdateIDAppliedToClient should be applied. + return Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); }) + .then(() => ApplyUpdates.applyUpdatesTriggeredPromise) .then(waitForBatchedUpdates) .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. expect(lastUpdateIDAppliedToClient).toBe(6); - expect(applyUpdates).toHaveBeenCalledTimes(1); + + // There should be only one call to applyUpdates. The call should only contain the last deferred update, + // since the locally applied updates have changed in the meantime. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); // eslint-disable-next-line @typescript-eslint/naming-convention - expect(applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); }); }); + + it('should re-fetch missing updates if the deferred updates have a gap', async () => { + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); + + return ( + App.getMissingOnyxUpdatesTriggeredPromise + .then(() => { + // All scheduled updates should have been added to the deferred list + expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); + }) + // The applicable updates (only update 3) should be applied first + .then(() => ApplyUpdates.applyUpdatesTriggeredPromise) + // After applying the applicable updates, the missing updates from 3 to 4 should be re-fetched + .then(() => App.getMissingOnyxUpdatesTriggeredPromise) + .then(() => { + // The deferred updates after the gap should now be in the list of dererred updates. + expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(2); + }) + .then(waitForBatchedUpdates) + .then(() => OnyxUpdateManager.queryPromise) + ?.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(6); + + // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. + // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + + // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5, 6: mockUpdate6}); + }) + ); + }); }); From aea045bcc07c64c32b1516678ddbf9eb76d05c0c Mon Sep 17 00:00:00 2001 From: Stephanie Elliott <31225194+stephanieelliott@users.noreply.github.com> Date: Mon, 22 Apr 2024 09:34:43 -0400 Subject: [PATCH 281/580] Update Submit-or-retract-a-report.md Update for Instant Submit for Collect workspaces --- .../expensify-classic/reports/Submit-or-retract-a-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md b/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md index 857217189e50..aa5aea545a23 100644 --- a/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md +++ b/docs/articles/expensify-classic/reports/Submit-or-retract-a-report.md @@ -60,6 +60,8 @@ You can retract a submitted report to edit the reported expenses and re-submit t 4. Tap **Retract** at the top of the report. {% include end-option.html %} +**Note:** Workspaces with Instant Submit set as the Scheduled Submit frequency won’t have the option to Retract entire reports, only individual expenses. + {% include end-selector.html %} </div> From 636f82555ac538331120ae42097fac48078c4646 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar <monil@expensify.com> Date: Mon, 22 Apr 2024 19:44:43 +0530 Subject: [PATCH 282/580] Use fallback route --- src/libs/actions/Report.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 8b0dbf8a37a9..3e83281dc83f 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2468,7 +2468,8 @@ function navigateToMostRecentReport(currentReport: OnyxEntry<Report>) { if (lastAccessedReportID) { // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to last accessed report. if (!isChatThread) { - Navigation.goBack(); + // Fallback to the lastAccessedReportID route, if this is first route in the navigator + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '')); } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? ''), CONST.NAVIGATION.TYPE.FORCED_UP); } else { From 4f6ee19fc01969cb7748648fb33075ded4e7f71b Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Mon, 22 Apr 2024 16:53:53 +0200 Subject: [PATCH 283/580] add spanish translations --- src/languages/es.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index d512a39deecb..ff03d1b9d26e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1068,15 +1068,15 @@ export default { expensifyCard: 'Tarjeta Expensify', availableSpend: 'Límite restante', smartLimit: { - name: 'Smart limit', + name: 'Límite inteligente', title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta al mes. El límite se restablecerá el primer día del mes.`, }, fixedLimit: { - name: 'Fixed limit', + name: 'Límite fijo', title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta, luego se desactivará.`, }, monthlyLimit: { - name: 'Monthly limit', + name: 'Límite mensual', title: (formattedLimit: string) => `Puedes gastar hasta ${formattedLimit} en esta tarjeta y el límite se restablecerá a medida que se aprueben tus gastos.`, }, virtualCardNumber: 'Número de la tarjeta virtual', From 4eaf83a4f0f094d3a5241686168a1f3f0ffd3ec7 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 22 Apr 2024 17:41:12 +0200 Subject: [PATCH 284/580] apply updaes --- .../utils/__mocks__/applyUpdates.ts | 29 +-- .../utils/deferredUpdates.ts | 2 +- .../actions/OnyxUpdateManager/utils/index.ts | 14 +- src/libs/actions/__mocks__/App.ts | 32 +-- src/utils/createProxyForValue.ts | 15 +- src/utils/createTriggerPromise.ts | 12 -- tests/unit/OnyxUpdateManagerTest.ts | 189 ++++++++++++------ tests/utils/createTriggerPromise.ts | 44 ++++ 8 files changed, 218 insertions(+), 119 deletions(-) delete mode 100644 src/utils/createTriggerPromise.ts create mode 100644 tests/utils/createTriggerPromise.ts diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts index a5a916cadfe4..43a96a757b98 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts @@ -1,32 +1,35 @@ import Onyx from 'react-native-onyx'; import type DeferredUpdatesDictionary from '@libs/actions/OnyxUpdateManager/types'; +import createTriggerPromise from '@src/../tests/utils/createTriggerPromise'; import ONYXKEYS from '@src/ONYXKEYS'; -import createTriggerPromise from '@src/utils/createTriggerPromise'; +import createProxyForValue from '@src/utils/createProxyForValue'; -const {promise: applyUpdatesTriggeredPromise, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggeredPromise} = createTriggerPromise(); +const {initialPromises: initialApplyUpdatesTriggeredPromises, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggered} = createTriggerPromise(); -const applyUpdates = jest.fn((updates: DeferredUpdatesDictionary) => { - console.log('apply updates'); +const mockValues = { + applyUpdatesTriggered: initialApplyUpdatesTriggeredPromises, +}; +const mockValuesProxy = createProxyForValue(mockValues); + +const resetApplyUpdatesTriggeredPromise = () => + resetApplyUpdatesTriggered((newPromise, index) => { + mockValuesProxy.applyUpdatesTriggered[index] = newPromise; + }); +const applyUpdates = jest.fn((updates: DeferredUpdatesDictionary) => { applyUpdatesTriggered(); const lastUpdateIdFromUpdates = Math.max(...Object.keys(updates).map(Number)); + console.log({lastUpdateIdFromUpdates}); + const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIdFromUpdates); promise.finally(() => { resetApplyUpdatesTriggeredPromise(); }); - promise - .then(() => { - console.log('applyUpdates succeeded'); - }) - .catch((e) => { - console.log('applyUpdates failed', e); - }); - return promise; }); -export {applyUpdates, applyUpdatesTriggeredPromise}; +export {applyUpdates, mockValuesProxy}; diff --git a/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts index 4463e8ea5a7b..d27aacff9911 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts @@ -3,6 +3,6 @@ import createProxyForValue from '@src/utils/createProxyForValue'; const deferredUpdatesValue = {deferredUpdates: {} as DeferredUpdatesDictionary}; -const deferredUpdatesProxy = createProxyForValue(deferredUpdatesValue, 'deferredUpdates'); +const deferredUpdatesProxy = createProxyForValue(deferredUpdatesValue); export default deferredUpdatesProxy; diff --git a/src/libs/actions/OnyxUpdateManager/utils/index.ts b/src/libs/actions/OnyxUpdateManager/utils/index.ts index 0ca42a46db7a..6f45d6d64816 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/index.ts @@ -14,7 +14,9 @@ Onyx.connect({ // In order for the deferred updates to be applied correctly in order, // we need to check if there are any gaps between deferred updates. type DetectGapAndSplitResult = {applicableUpdates: DeferredUpdatesDictionary; updatesAfterGaps: DeferredUpdatesDictionary; latestMissingUpdateID: number | undefined}; -function detectGapsAndSplit(updates: DeferredUpdatesDictionary, lastUpdateIDFromClient: number): DetectGapAndSplitResult { +function detectGapsAndSplit(updates: DeferredUpdatesDictionary, clientLastUpdateID?: number): DetectGapAndSplitResult { + const lastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0; + const updateValues = Object.values(updates); const applicableUpdates: DeferredUpdatesDictionary = {}; @@ -103,21 +105,19 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number): Promise<v return new Promise((resolve, reject) => { deferredUpdatesProxy.deferredUpdates = {}; - console.log({applicableUpdates}); - applyUpdates(applicableUpdates).then(() => { // After we have applied the applicable updates, there might have been new deferred updates added. // In the next (recursive) call of "validateAndApplyDeferredUpdates", // the initial "updatesAfterGaps" and all new deferred updates will be applied in order, // as long as there was no new gap detected. Otherwise repeat the process. - console.log({updatesAfterGaps}); + const newLastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0; + deferredUpdatesProxy.deferredUpdates = {...deferredUpdatesProxy.deferredUpdates, ...updatesAfterGaps}; // updateDeferredUpdates(newDeferredUpdates); - // It should not be possible for lastUpdateIDAppliedToClient to be null, therefore we can ignore this case. // If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates. - if (latestMissingUpdateID <= lastUpdateIDFromClient) { + if (latestMissingUpdateID <= newLastUpdateIDFromClient) { validateAndApplyDeferredUpdates(clientLastUpdateID) .then(() => resolve(undefined)) .catch(reject); @@ -125,7 +125,7 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number): Promise<v } // Then we can fetch the missing updates and apply them - App.getMissingOnyxUpdates(lastUpdateIDFromClient, latestMissingUpdateID) + App.getMissingOnyxUpdates(newLastUpdateIDFromClient, latestMissingUpdateID) .then(() => validateAndApplyDeferredUpdates(clientLastUpdateID)) .then(() => resolve(undefined)) .catch(reject); diff --git a/src/libs/actions/__mocks__/App.ts b/src/libs/actions/__mocks__/App.ts index 81f3cbec41f3..443103ad7209 100644 --- a/src/libs/actions/__mocks__/App.ts +++ b/src/libs/actions/__mocks__/App.ts @@ -1,8 +1,8 @@ import Onyx from 'react-native-onyx'; import type * as AppImport from '@libs/actions/App'; +import createTriggerPromise from '@src/../tests/utils/createTriggerPromise'; import ONYXKEYS from '@src/ONYXKEYS'; import createProxyForValue from '@src/utils/createProxyForValue'; -import createTriggerPromise from '@src/utils/createTriggerPromise'; const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); const { @@ -25,21 +25,29 @@ const { KEYS_TO_PRESERVE, } = AppImplementation; -const shouldGetMissingOnyxUpdatesUpToIdValue = {shouldGetMissingOnyxUpdatesUpToId: 2}; -const shouldGetMissingOnyxUpdatesUpToIdProxy = createProxyForValue(shouldGetMissingOnyxUpdatesUpToIdValue, 'shouldGetMissingOnyxUpdatesUpToId'); -const {shouldGetMissingOnyxUpdatesUpToId} = shouldGetMissingOnyxUpdatesUpToIdValue; +const { + initialPromises: initialGetMissingOnyxUpdatesTriggeredPromises, + trigger: getMissingOnyxUpdatesWasTriggered, + resetPromise: resetGetMissingOnyxUpdatesTriggered, +} = createTriggerPromise(); + +const mockValues = { + getMissingOnyxUpdatesTriggered: initialGetMissingOnyxUpdatesTriggeredPromises, +}; +const mockValuesProxy = createProxyForValue(mockValues); -const {promise: getMissingOnyxUpdatesTriggeredPromise, trigger: getMissingOnyxUpdatesWasTriggered, resetPromise: resetGetMissingOnyxUpdatesTriggeredPromise} = createTriggerPromise(); -const {promise: getMissingOnyxUpdatesDonePromise, trigger: getMissingOnyxUpdatesDone, resetPromise: resetGetMissingOnyxUpdatesDonePromise} = createTriggerPromise(); +const resetGetMissingOnyxUpdatesTriggeredPromise = () => { + resetGetMissingOnyxUpdatesTriggered((newPromise, index) => { + mockValuesProxy.getMissingOnyxUpdatesTriggered[index] = newPromise; + }); +}; -const getMissingOnyxUpdates = jest.fn(() => { - resetGetMissingOnyxUpdatesDonePromise(); +const getMissingOnyxUpdates = jest.fn((_fromID: number, toID: number) => { getMissingOnyxUpdatesWasTriggered(); - const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, shouldGetMissingOnyxUpdatesUpToId); + const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, toID); promise.finally(() => { - getMissingOnyxUpdatesDone(); resetGetMissingOnyxUpdatesTriggeredPromise(); }); @@ -48,9 +56,7 @@ const getMissingOnyxUpdates = jest.fn(() => { export { // Mocks - shouldGetMissingOnyxUpdatesUpToIdProxy, - getMissingOnyxUpdatesTriggeredPromise, - getMissingOnyxUpdatesDonePromise, + mockValuesProxy, getMissingOnyxUpdates, // Actual App implementation diff --git a/src/utils/createProxyForValue.ts b/src/utils/createProxyForValue.ts index e9e8eb5dca6b..ace0951aa720 100644 --- a/src/utils/createProxyForValue.ts +++ b/src/utils/createProxyForValue.ts @@ -1,17 +1,18 @@ -const createProxyForValue = <Value, VariableName extends string>(value: Record<VariableName, Value>, variableName: VariableName) => +const createProxyForValue = <Value extends Record<string, unknown>>(value: Value) => new Proxy(value, { - get: (target, prop) => { - if (prop !== variableName) { + get: (target, property) => { + if (typeof property === 'symbol') { return undefined; } - return target[prop as VariableName]; + + return target[property]; }, - set: (target, prop, newValue) => { - if (prop !== variableName) { + set: (target, property, newValue) => { + if (typeof property === 'symbol') { return false; } // eslint-disable-next-line no-param-reassign - target[prop as VariableName] = newValue; + target[property as keyof Value] = newValue; return true; }, }); diff --git a/src/utils/createTriggerPromise.ts b/src/utils/createTriggerPromise.ts deleted file mode 100644 index 9060ba22ec86..000000000000 --- a/src/utils/createTriggerPromise.ts +++ /dev/null @@ -1,12 +0,0 @@ -const createTriggerPromise = () => { - let trigger: () => void = () => undefined; - const resetPromise = () => - new Promise<void>((resolve) => { - trigger = resolve; - }); - const promise = resetPromise(); - - return {promise, trigger, resetPromise}; -}; - -export default createTriggerPromise; diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 996891f725c6..c0923ce2f678 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -11,15 +11,17 @@ jest.mock('@libs/actions/App'); jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); type AppActionsMock = typeof AppImport & { - shouldGetMissingOnyxUpdatesUpToIdProxy: Record<'shouldGetMissingOnyxUpdatesUpToId', number>; getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; - getMissingOnyxUpdatesTriggeredPromise: Promise<void>; - getMissingOnyxUpdatesDonePromise: Promise<void>; + mockValuesProxy: { + getMissingOnyxUpdatesTriggered: Array<Promise<void>>; + }; }; type ApplyUpdatesMock = typeof ApplyUpdatesImport & { applyUpdates: jest.Mock<Promise<void[]>>; - applyUpdatesTriggeredPromise: Promise<void>; + mockValuesProxy: { + applyUpdatesTriggered: Promise<void>; + }; }; const App = AppImport as AppActionsMock; @@ -53,22 +55,22 @@ const mockUpdate5 = createMockUpdate(5); const mockUpdate6 = createMockUpdate(6); const resetOnyxUpdateManager = async () => { - App.shouldGetMissingOnyxUpdatesUpToIdProxy.shouldGetMissingOnyxUpdatesUpToId = 2; await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); OnyxUpdateManager.resetDeferralLogicVariables(); }; describe('OnyxUpdateManager', () => { let lastUpdateIDAppliedToClient = 1; - - beforeEach(async () => { - jest.clearAllMocks(); - Onyx.clear(); - await resetOnyxUpdateManager(); + beforeAll(() => { Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, callback: (value) => (lastUpdateIDAppliedToClient = value ?? 1), }); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await resetOnyxUpdateManager(); return waitForBatchedUpdates(); }); @@ -77,14 +79,27 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); - return App.getMissingOnyxUpdatesTriggeredPromise - .then(() => { - // Missing updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) should have been fetched from the server - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); - // All scheduled updates should have been added to the deferred list - expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); - }) - .then(waitForBatchedUpdates) + // return App.mockValuesProxy.getMissingOnyxUpdatesTriggered[0] + // .then(() => { + // // Missing updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) should have been fetched from the server + // expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); + // // All scheduled updates should have been added to the deferred list + // expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); + // }) + // .then(waitForBatchedUpdates) + // .then(() => OnyxUpdateManager.queryPromise) + // ?.then(() => { + // // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + // expect(lastUpdateIDAppliedToClient).toBe(5); + + // // There should be only one call to applyUpdates. The call should contain all the deferred update, + // // since the locally applied updates have changed in the meantime. + // expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + // // eslint-disable-next-line @typescript-eslint/naming-convention + // expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); + // }); + + return waitForBatchedUpdates() .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. @@ -99,34 +114,51 @@ describe('OnyxUpdateManager', () => { }); it('should only apply deferred updates that are after the locally applied update (pending updates)', async () => { - App.shouldGetMissingOnyxUpdatesUpToIdProxy.shouldGetMissingOnyxUpdatesUpToId = 3; - OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); - return App.getMissingOnyxUpdatesTriggeredPromise - .then(() => { - // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); - // All scheduled updates should have been added to the deferred list - expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); - - // We manually update the lastUpdateIDAppliedToClient to 5, to simulate local updates being applied, - // while we are waiting for the missing updates to be fetched. - // Only the deferred updates after the lastUpdateIDAppliedToClient should be applied. - return Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); - }) - .then(() => ApplyUpdates.applyUpdatesTriggeredPromise) - .then(waitForBatchedUpdates) + // We manually update the lastUpdateIDAppliedToClient to 5, to simulate local updates being applied, + // while we are waiting for the missing updates to be fetched. + // Only the deferred updates after the lastUpdateIDAppliedToClient should be applied. + Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); + + // return App.mockValuesProxy.getMissingOnyxUpdatesTriggered[0] + // .then(() => { + // // All scheduled updates should have been added to the deferred list + // expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); + // }) + // .then(() => ApplyUpdates.mockValuesProxy.applyUpdatesTriggered) + // .then(waitForBatchedUpdates) + // .then(() => OnyxUpdateManager.queryPromise) + // ?.then(() => { + // // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + // expect(lastUpdateIDAppliedToClient).toBe(6); + + // // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server in the first and only call to getMissingOnyxUpdates + // expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); + + // // There should be only one call to applyUpdates. The call should only contain the last deferred update, + // // since the locally applied updates have changed in the meantime. + // expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + + // // eslint-disable-next-line @typescript-eslint/naming-convention + // expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); + // }); + + return waitForBatchedUpdates() .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. expect(lastUpdateIDAppliedToClient).toBe(6); + // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server in the first and only call to getMissingOnyxUpdates + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); + // There should be only one call to applyUpdates. The call should only contain the last deferred update, // since the locally applied updates have changed in the meantime. expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/naming-convention expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); }); @@ -137,37 +169,62 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); - return ( - App.getMissingOnyxUpdatesTriggeredPromise - .then(() => { - // All scheduled updates should have been added to the deferred list - expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); - }) - // The applicable updates (only update 3) should be applied first - .then(() => ApplyUpdates.applyUpdatesTriggeredPromise) - // After applying the applicable updates, the missing updates from 3 to 4 should be re-fetched - .then(() => App.getMissingOnyxUpdatesTriggeredPromise) - .then(() => { - // The deferred updates after the gap should now be in the list of dererred updates. - expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(2); - }) - .then(waitForBatchedUpdates) - .then(() => OnyxUpdateManager.queryPromise) - ?.then(() => { - // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - expect(lastUpdateIDAppliedToClient).toBe(6); - - // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. - // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); - - // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5, 6: mockUpdate6}); - }) - ); + // return ( + // App.mockValuesProxy.getMissingOnyxUpdatesTriggered[0] + // .then(() => { + // // All scheduled updates should have been added to the deferred list + // expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); + // }) + // // The applicable updates (only update 3) should be applied first + // .then(() => ApplyUpdates.mockValuesProxy.applyUpdatesTriggered) + // // After applying the applicable updates, the missing updates from 3 to 4 should be re-fetched + // .then(() => { + // App.mockValuesProxy.shouldGetMissingOnyxUpdatesUpToId = 4; + // }) + // .then(() => App.mockValuesProxy.getMissingOnyxUpdatesTriggered[1]) + // .then(() => { + // // The deferred updates after the gap should now be in the list of dererred updates. + // expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(2); + // }) + // .then(waitForBatchedUpdates) + // .then(() => OnyxUpdateManager.queryPromise) + // ?.then(() => { + // // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + // expect(lastUpdateIDAppliedToClient).toBe(6); + + // // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. + // // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. + // expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); + // // eslint-disable-next-line @typescript-eslint/naming-convention + // expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + + // // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. + // expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); + // // eslint-disable-next-line @typescript-eslint/naming-convention + // expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5, 6: mockUpdate6}); + // }) + // ); + + return waitForBatchedUpdates() + .then(() => OnyxUpdateManager.queryPromise) + ?.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(6); + + // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. + // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); + + // After the initial missing updates have been applied, the applicable updates (3) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + + // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap in the deferred updates. 3-4 + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); + + // After the gap in the deferred updates has been resolve, the remaining deferred updates (5, 6) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5, 6: mockUpdate6}); + }); }); }); diff --git a/tests/utils/createTriggerPromise.ts b/tests/utils/createTriggerPromise.ts new file mode 100644 index 000000000000..8afc4f06ef74 --- /dev/null +++ b/tests/utils/createTriggerPromise.ts @@ -0,0 +1,44 @@ +const createTriggerPromise = (count = 1) => { + let promiseIndex = 0; + let resolves: Array<() => void> = []; + const initialPromises = Array(count) + .fill(0) + .map( + (index) => + new Promise<void>((resolve) => { + if (index !== 0) { + return; + } + resolves.push(resolve); + }), + ); + + let trigger: () => void = () => undefined; + + const resetPromise = (resetPromiseCallback?: (resettedPromise: Promise<void>, index: number) => void) => { + const newPromise = new Promise<void>((resolve) => { + trigger = resolve; + }); + + if (resetPromiseCallback) { + return resetPromiseCallback(newPromise, promiseIndex); + } + + initialPromises[promiseIndex] = newPromise; + if (promiseIndex < count - 1) { + promiseIndex++; + } + }; + + if (resolves.length === 0) { + resetPromise(); + } else { + trigger = resolves[0]; + resolves = []; + } + + resetPromise(); + return {initialPromises, trigger, resetPromise}; +}; + +export default createTriggerPromise; From 99109431ead2f4287a7d151fee3c8fc33f4301db Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 22 Apr 2024 17:45:43 +0200 Subject: [PATCH 285/580] remove unnecessary promise trigger code --- .../utils/__mocks__/applyUpdates.ts | 27 +----- src/libs/actions/__mocks__/App.ts | 27 ------ tests/unit/OnyxUpdateManagerTest.ts | 86 ------------------- tests/utils/createTriggerPromise.ts | 44 ---------- 4 files changed, 2 insertions(+), 182 deletions(-) delete mode 100644 tests/utils/createTriggerPromise.ts diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts index 43a96a757b98..1a3f0f3e8a95 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts @@ -1,35 +1,12 @@ import Onyx from 'react-native-onyx'; import type DeferredUpdatesDictionary from '@libs/actions/OnyxUpdateManager/types'; -import createTriggerPromise from '@src/../tests/utils/createTriggerPromise'; import ONYXKEYS from '@src/ONYXKEYS'; -import createProxyForValue from '@src/utils/createProxyForValue'; - -const {initialPromises: initialApplyUpdatesTriggeredPromises, trigger: applyUpdatesTriggered, resetPromise: resetApplyUpdatesTriggered} = createTriggerPromise(); - -const mockValues = { - applyUpdatesTriggered: initialApplyUpdatesTriggeredPromises, -}; -const mockValuesProxy = createProxyForValue(mockValues); - -const resetApplyUpdatesTriggeredPromise = () => - resetApplyUpdatesTriggered((newPromise, index) => { - mockValuesProxy.applyUpdatesTriggered[index] = newPromise; - }); const applyUpdates = jest.fn((updates: DeferredUpdatesDictionary) => { - applyUpdatesTriggered(); - const lastUpdateIdFromUpdates = Math.max(...Object.keys(updates).map(Number)); - - console.log({lastUpdateIdFromUpdates}); - const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIdFromUpdates); - - promise.finally(() => { - resetApplyUpdatesTriggeredPromise(); - }); - return promise; }); -export {applyUpdates, mockValuesProxy}; +// eslint-disable-next-line import/prefer-default-export +export {applyUpdates}; diff --git a/src/libs/actions/__mocks__/App.ts b/src/libs/actions/__mocks__/App.ts index 443103ad7209..6b834cf4f371 100644 --- a/src/libs/actions/__mocks__/App.ts +++ b/src/libs/actions/__mocks__/App.ts @@ -1,8 +1,6 @@ import Onyx from 'react-native-onyx'; import type * as AppImport from '@libs/actions/App'; -import createTriggerPromise from '@src/../tests/utils/createTriggerPromise'; import ONYXKEYS from '@src/ONYXKEYS'; -import createProxyForValue from '@src/utils/createProxyForValue'; const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); const { @@ -25,38 +23,13 @@ const { KEYS_TO_PRESERVE, } = AppImplementation; -const { - initialPromises: initialGetMissingOnyxUpdatesTriggeredPromises, - trigger: getMissingOnyxUpdatesWasTriggered, - resetPromise: resetGetMissingOnyxUpdatesTriggered, -} = createTriggerPromise(); - -const mockValues = { - getMissingOnyxUpdatesTriggered: initialGetMissingOnyxUpdatesTriggeredPromises, -}; -const mockValuesProxy = createProxyForValue(mockValues); - -const resetGetMissingOnyxUpdatesTriggeredPromise = () => { - resetGetMissingOnyxUpdatesTriggered((newPromise, index) => { - mockValuesProxy.getMissingOnyxUpdatesTriggered[index] = newPromise; - }); -}; - const getMissingOnyxUpdates = jest.fn((_fromID: number, toID: number) => { - getMissingOnyxUpdatesWasTriggered(); - const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, toID); - - promise.finally(() => { - resetGetMissingOnyxUpdatesTriggeredPromise(); - }); - return promise; }); export { // Mocks - mockValuesProxy, getMissingOnyxUpdates, // Actual App implementation diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index c0923ce2f678..ca7fefb4c28e 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -2,7 +2,6 @@ import Onyx from 'react-native-onyx'; import * as AppImport from '@libs/actions/App'; import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; -import deferredUpdatesProxy from '@libs/actions/OnyxUpdateManager/utils/deferredUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -12,16 +11,10 @@ jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); type AppActionsMock = typeof AppImport & { getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; - mockValuesProxy: { - getMissingOnyxUpdatesTriggered: Array<Promise<void>>; - }; }; type ApplyUpdatesMock = typeof ApplyUpdatesImport & { applyUpdates: jest.Mock<Promise<void[]>>; - mockValuesProxy: { - applyUpdatesTriggered: Promise<void>; - }; }; const App = AppImport as AppActionsMock; @@ -79,26 +72,6 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); - // return App.mockValuesProxy.getMissingOnyxUpdatesTriggered[0] - // .then(() => { - // // Missing updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) should have been fetched from the server - // expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); - // // All scheduled updates should have been added to the deferred list - // expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); - // }) - // .then(waitForBatchedUpdates) - // .then(() => OnyxUpdateManager.queryPromise) - // ?.then(() => { - // // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - // expect(lastUpdateIDAppliedToClient).toBe(5); - - // // There should be only one call to applyUpdates. The call should contain all the deferred update, - // // since the locally applied updates have changed in the meantime. - // expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); - // // eslint-disable-next-line @typescript-eslint/naming-convention - // expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); - // }); - return waitForBatchedUpdates() .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { @@ -123,29 +96,6 @@ describe('OnyxUpdateManager', () => { // Only the deferred updates after the lastUpdateIDAppliedToClient should be applied. Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); - // return App.mockValuesProxy.getMissingOnyxUpdatesTriggered[0] - // .then(() => { - // // All scheduled updates should have been added to the deferred list - // expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); - // }) - // .then(() => ApplyUpdates.mockValuesProxy.applyUpdatesTriggered) - // .then(waitForBatchedUpdates) - // .then(() => OnyxUpdateManager.queryPromise) - // ?.then(() => { - // // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - // expect(lastUpdateIDAppliedToClient).toBe(6); - - // // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server in the first and only call to getMissingOnyxUpdates - // expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); - - // // There should be only one call to applyUpdates. The call should only contain the last deferred update, - // // since the locally applied updates have changed in the meantime. - // expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); - - // // eslint-disable-next-line @typescript-eslint/naming-convention - // expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); - // }); - return waitForBatchedUpdates() .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { @@ -169,42 +119,6 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); - // return ( - // App.mockValuesProxy.getMissingOnyxUpdatesTriggered[0] - // .then(() => { - // // All scheduled updates should have been added to the deferred list - // expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(3); - // }) - // // The applicable updates (only update 3) should be applied first - // .then(() => ApplyUpdates.mockValuesProxy.applyUpdatesTriggered) - // // After applying the applicable updates, the missing updates from 3 to 4 should be re-fetched - // .then(() => { - // App.mockValuesProxy.shouldGetMissingOnyxUpdatesUpToId = 4; - // }) - // .then(() => App.mockValuesProxy.getMissingOnyxUpdatesTriggered[1]) - // .then(() => { - // // The deferred updates after the gap should now be in the list of dererred updates. - // expect(Object.keys(deferredUpdatesProxy.deferredUpdates)).toHaveLength(2); - // }) - // .then(waitForBatchedUpdates) - // .then(() => OnyxUpdateManager.queryPromise) - // ?.then(() => { - // // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - // expect(lastUpdateIDAppliedToClient).toBe(6); - - // // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. - // // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. - // expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); - // // eslint-disable-next-line @typescript-eslint/naming-convention - // expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); - - // // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. - // expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); - // // eslint-disable-next-line @typescript-eslint/naming-convention - // expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5, 6: mockUpdate6}); - // }) - // ); - return waitForBatchedUpdates() .then(() => OnyxUpdateManager.queryPromise) ?.then(() => { diff --git a/tests/utils/createTriggerPromise.ts b/tests/utils/createTriggerPromise.ts deleted file mode 100644 index 8afc4f06ef74..000000000000 --- a/tests/utils/createTriggerPromise.ts +++ /dev/null @@ -1,44 +0,0 @@ -const createTriggerPromise = (count = 1) => { - let promiseIndex = 0; - let resolves: Array<() => void> = []; - const initialPromises = Array(count) - .fill(0) - .map( - (index) => - new Promise<void>((resolve) => { - if (index !== 0) { - return; - } - resolves.push(resolve); - }), - ); - - let trigger: () => void = () => undefined; - - const resetPromise = (resetPromiseCallback?: (resettedPromise: Promise<void>, index: number) => void) => { - const newPromise = new Promise<void>((resolve) => { - trigger = resolve; - }); - - if (resetPromiseCallback) { - return resetPromiseCallback(newPromise, promiseIndex); - } - - initialPromises[promiseIndex] = newPromise; - if (promiseIndex < count - 1) { - promiseIndex++; - } - }; - - if (resolves.length === 0) { - resetPromise(); - } else { - trigger = resolves[0]; - resolves = []; - } - - resetPromise(); - return {initialPromises, trigger, resetPromise}; -}; - -export default createTriggerPromise; From 0b6c1d61e1fa9fdc23574ce646f551fbaa476357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Mon, 22 Apr 2024 18:18:25 +0200 Subject: [PATCH 286/580] Remove numberOfLines calculations and fix composer padding --- src/CONST.ts | 5 ++--- src/components/Composer/index.native.tsx | 4 ++-- src/components/Composer/index.tsx | 5 +++-- .../getNumberOfLines/index.native.ts | 8 -------- .../ComposerUtils/getNumberOfLines/index.ts | 12 ----------- .../ComposerUtils/getNumberOfLines/types.ts | 3 --- src/libs/ComposerUtils/index.ts | 4 +--- .../updateIsFullComposerAvailable.ts | 12 +++++++++-- .../updateNumberOfLines/index.ts | 20 ------------------- .../updateNumberOfLines/types.ts | 7 ------- src/styles/utils/index.ts | 10 ++-------- 11 files changed, 20 insertions(+), 70 deletions(-) delete mode 100644 src/libs/ComposerUtils/getNumberOfLines/index.native.ts delete mode 100644 src/libs/ComposerUtils/getNumberOfLines/index.ts delete mode 100644 src/libs/ComposerUtils/getNumberOfLines/types.ts delete mode 100644 src/libs/ComposerUtils/updateNumberOfLines/index.ts delete mode 100644 src/libs/ComposerUtils/updateNumberOfLines/types.ts diff --git a/src/CONST.ts b/src/CONST.ts index ab5a67274955..bf009a48a77c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -848,9 +848,8 @@ const CONST = { MAX_LINES: 16, MAX_LINES_SMALL_SCREEN: 6, MAX_LINES_FULL: -1, - - // The minimum number of typed lines needed to enable the full screen composer - FULL_COMPOSER_MIN_LINES: 3, + // The minimum height needed to enable the full screen composer + FULL_COMPOSER_MIN_HEIGHT: 66, }, MODAL: { MODAL_TYPE: { diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 3abb88a6cb26..4d135cdd88e2 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -9,7 +9,7 @@ import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ComposerUtils from '@libs/ComposerUtils'; +import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import type {ComposerProps} from './types'; function Composer( @@ -74,7 +74,7 @@ function Composer( placeholderTextColor={theme.placeholderText} ref={setTextInputRef} value={value} - onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles)} + onContentSizeChange={(e) => updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles, true)} rejectResponderTermination={false} smartInsertDelete={false} textAlignVertical="center" diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 59f7333433bb..43a100cf3c90 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -15,7 +15,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; -import * as ComposerUtils from '@libs/ComposerUtils'; +import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; @@ -311,6 +311,7 @@ function Composer( const inputStyleMemo = useMemo( () => [ StyleSheet.flatten([style, {outline: 'none'}]), + StyleUtils.getComposeTextAreaPadding(isComposerFullSize), Browser.isMobileSafari() || Browser.isSafari() ? styles.rtlTextRenderForSafari : {}, scrollStyleMemo, StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), @@ -338,7 +339,7 @@ function Composer( onSelectionChange={addCursorPositionToSelectionChange} onContentSizeChange={(e) => { setTextInputWidth(`${e.nativeEvent.contentSize.width}px`); - ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); + updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); }} disabled={isDisabled} onKeyPress={handleKeyPress} diff --git a/src/libs/ComposerUtils/getNumberOfLines/index.native.ts b/src/libs/ComposerUtils/getNumberOfLines/index.native.ts deleted file mode 100644 index 0cbfb6c7f517..000000000000 --- a/src/libs/ComposerUtils/getNumberOfLines/index.native.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type GetNumberOfLines from './types'; - -/** - * Get the current number of lines in the composer - */ -const getNumberOfLines: GetNumberOfLines = (lineHeight, paddingTopAndBottom, scrollHeight) => Math.ceil((scrollHeight - paddingTopAndBottom) / lineHeight); - -export default getNumberOfLines; diff --git a/src/libs/ComposerUtils/getNumberOfLines/index.ts b/src/libs/ComposerUtils/getNumberOfLines/index.ts deleted file mode 100644 index e80744d41c69..000000000000 --- a/src/libs/ComposerUtils/getNumberOfLines/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type GetNumberOfLines from './types'; - -/** - * Get the current number of lines in the composer - */ -const getNumberOfLines: GetNumberOfLines = (lineHeight, paddingTopAndBottom, scrollHeight, maxLines = 0) => { - let newNumberOfLines = Math.ceil((scrollHeight - paddingTopAndBottom) / lineHeight); - newNumberOfLines = maxLines <= 0 ? newNumberOfLines : Math.min(newNumberOfLines, maxLines); - return newNumberOfLines; -}; - -export default getNumberOfLines; diff --git a/src/libs/ComposerUtils/getNumberOfLines/types.ts b/src/libs/ComposerUtils/getNumberOfLines/types.ts deleted file mode 100644 index 67bb790f726b..000000000000 --- a/src/libs/ComposerUtils/getNumberOfLines/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -type GetNumberOfLines = (lineHeight: number, paddingTopAndBottom: number, scrollHeight: number, maxLines?: number) => number; - -export default GetNumberOfLines; diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index f2e940abeb73..04d857a8faeb 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -1,6 +1,4 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import getNumberOfLines from './getNumberOfLines'; -import updateNumberOfLines from './updateNumberOfLines'; type Selection = { start: number; @@ -49,5 +47,5 @@ function findCommonSuffixLength(str1: string, str2: string, cursorPosition: numb return commonSuffixLength; } -export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys, insertWhiteSpaceAtIndex, findCommonSuffixLength}; +export {insertText, canSkipTriggerHotkeys, insertWhiteSpaceAtIndex, findCommonSuffixLength}; export type {Selection}; diff --git a/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts b/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts index 580908fc3805..c10635f1c491 100644 --- a/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts +++ b/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts @@ -1,12 +1,20 @@ +import type {NativeSyntheticEvent, TextInputContentSizeChangeEventData} from 'react-native'; import type {ComposerProps} from '@components/Composer/types'; +import type {ThemeStyles} from '@styles/index'; import CONST from '@src/CONST'; /** * Update isFullComposerAvailable if needed * @param numberOfLines The number of lines in the text input */ -function updateIsFullComposerAvailable(props: ComposerProps, numberOfLines: number) { - const isFullComposerAvailable = numberOfLines >= CONST.COMPOSER.FULL_COMPOSER_MIN_LINES; +function updateIsFullComposerAvailable(props: ComposerProps, event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>, styles: ThemeStyles, shouldIncludePadding = false) { + const paddingTopAndBottom = shouldIncludePadding ? styles.textInputComposeSpacing.paddingVertical * 2 : 0; + const inputHeight = event?.nativeEvent?.contentSize?.height ?? null; + if (!inputHeight) { + return; + } + const totalHeight = inputHeight + paddingTopAndBottom; + const isFullComposerAvailable = totalHeight >= CONST.COMPOSER.FULL_COMPOSER_MIN_HEIGHT; if (isFullComposerAvailable !== props.isFullComposerAvailable) { props.setIsFullComposerAvailable?.(isFullComposerAvailable); } diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.ts deleted file mode 100644 index 5a7676d8bfbd..000000000000 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import getNumberOfLines from '@libs/ComposerUtils/getNumberOfLines'; -import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; -import type UpdateNumberOfLines from './types'; - -/** - * Check the current scrollHeight of the textarea (minus any padding) and - * divide by line height to get the total number of rows for the textarea. - */ -const updateNumberOfLines: UpdateNumberOfLines = (props, event, styles) => { - const lineHeight = styles.textInputCompose.lineHeight ?? 0; - const paddingTopAndBottom = styles.textInputComposeSpacing.paddingVertical * 2; - const inputHeight = event?.nativeEvent?.contentSize?.height ?? null; - if (!inputHeight) { - return; - } - const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight); - updateIsFullComposerAvailable(props, numberOfLines); -}; - -export default updateNumberOfLines; diff --git a/src/libs/ComposerUtils/updateNumberOfLines/types.ts b/src/libs/ComposerUtils/updateNumberOfLines/types.ts deleted file mode 100644 index 06daee56a707..000000000000 --- a/src/libs/ComposerUtils/updateNumberOfLines/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {NativeSyntheticEvent, TextInputContentSizeChangeEventData} from 'react-native'; -import type {ComposerProps} from '@components/Composer/types'; -import type {ThemeStyles} from '@styles/index'; - -type UpdateNumberOfLines = (props: ComposerProps, event: NativeSyntheticEvent<TextInputContentSizeChangeEventData>, styles: ThemeStyles) => void; - -export default UpdateNumberOfLines; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index e9efc84e8807..760f43064a0e 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -940,17 +940,11 @@ function getEmojiPickerListHeight(isRenderingShortcutRow: boolean, windowHeight: /** * Returns padding vertical based on number of lines */ -function getComposeTextAreaPadding(numberOfLines: number, isComposerFullSize: boolean): TextStyle { +function getComposeTextAreaPadding(isComposerFullSize: boolean): TextStyle { let paddingValue = 5; // Issue #26222: If isComposerFullSize paddingValue will always be 5 to prevent padding jumps when adding multiple lines. if (!isComposerFullSize) { - if (numberOfLines === 1) { - paddingValue = 9; - } - // In case numberOfLines = 3, there will be a Expand Icon appearing at the top left, so it has to be recalculated so that the textArea can be full height - else if (numberOfLines === 3) { - paddingValue = 8; - } + paddingValue = 8; } return { paddingTop: paddingValue, From b078cfad52330c12cc7fce24d1f397b79f8204c6 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 22 Apr 2024 19:45:55 +0200 Subject: [PATCH 287/580] update tests and mocks --- src/libs/actions/OnyxUpdateManager/index.ts | 5 +- src/libs/actions/OnyxUpdateManager/types.ts | 4 +- .../utils/__mocks__/applyUpdates.ts | 10 +- .../utils/__mocks__/index.ts | 12 ++ .../OnyxUpdateManager/utils/applyUpdates.ts | 2 +- .../utils/deferredUpdates.ts | 2 +- .../actions/OnyxUpdateManager/utils/index.ts | 4 +- tests/unit/OnyxUpdateManagerTest.ts | 183 +++++++++++++++++- 8 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts diff --git a/src/libs/actions/OnyxUpdateManager/index.ts b/src/libs/actions/OnyxUpdateManager/index.ts index 0fc84222d414..d46f31fe952b 100644 --- a/src/libs/actions/OnyxUpdateManager/index.ts +++ b/src/libs/actions/OnyxUpdateManager/index.ts @@ -7,7 +7,6 @@ import * as App from '@userActions/App'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer, Response} from '@src/types/onyx'; import {isValidOnyxUpdateFromServer} from '@src/types/onyx/OnyxUpdatesFromServer'; -import type DeferredUpdatesDictionary from './types'; import * as OnyxUpdateManagerUtils from './utils'; import deferredUpdatesProxy from './utils/deferredUpdates'; @@ -146,6 +145,4 @@ export default () => { }); }; -export {handleOnyxUpdateGap}; -export {queryPromise, resetDeferralLogicVariables}; -export type {DeferredUpdatesDictionary}; +export {handleOnyxUpdateGap, queryPromise, resetDeferralLogicVariables}; diff --git a/src/libs/actions/OnyxUpdateManager/types.ts b/src/libs/actions/OnyxUpdateManager/types.ts index a081dff00665..119dfb82ba1e 100644 --- a/src/libs/actions/OnyxUpdateManager/types.ts +++ b/src/libs/actions/OnyxUpdateManager/types.ts @@ -2,4 +2,6 @@ import type {OnyxUpdatesFromServer} from '@src/types/onyx'; type DeferredUpdatesDictionary = Record<number, OnyxUpdatesFromServer>; -export default DeferredUpdatesDictionary; +type DetectGapAndSplitResult = {applicableUpdates: DeferredUpdatesDictionary; updatesAfterGaps: DeferredUpdatesDictionary; latestMissingUpdateID: number | undefined}; + +export type {DeferredUpdatesDictionary, DetectGapAndSplitResult}; diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts index 1a3f0f3e8a95..412214cdf2ec 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts @@ -1,10 +1,16 @@ import Onyx from 'react-native-onyx'; -import type DeferredUpdatesDictionary from '@libs/actions/OnyxUpdateManager/types'; +import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; import ONYXKEYS from '@src/ONYXKEYS'; +let lastUpdateIDAppliedToClient = 0; +Onyx.connect({ + key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, + callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), +}); + const applyUpdates = jest.fn((updates: DeferredUpdatesDictionary) => { const lastUpdateIdFromUpdates = Math.max(...Object.keys(updates).map(Number)); - const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, lastUpdateIdFromUpdates); + const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Math.max(lastUpdateIDAppliedToClient, lastUpdateIdFromUpdates)); return promise; }); diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts new file mode 100644 index 000000000000..3cfb0abefbeb --- /dev/null +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts @@ -0,0 +1,12 @@ +import type * as UtilsImport from '..'; + +const UtilsImplementation: typeof UtilsImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils'); + +const detectGapsAndSplit = jest.fn(UtilsImplementation.detectGapsAndSplit); +const validateAndApplyDeferredUpdates = jest.fn(UtilsImplementation.validateAndApplyDeferredUpdates); + +export { + // Mocks + detectGapsAndSplit, + validateAndApplyDeferredUpdates, +}; diff --git a/src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts index 37a1ec70728d..5857b079c1ba 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/applyUpdates.ts @@ -1,5 +1,5 @@ // We need to keep this in a separate file, so that we can mock this function in tests. -import type DeferredUpdatesDictionary from '@libs/actions/OnyxUpdateManager/types'; +import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; import * as OnyxUpdates from '@userActions/OnyxUpdates'; // This function applies a list of updates to Onyx in order and resolves when all updates have been applied diff --git a/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts index d27aacff9911..1bf251fbd3df 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts @@ -1,4 +1,4 @@ -import type DeferredUpdatesDictionary from '@libs/actions/OnyxUpdateManager/types'; +import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; import createProxyForValue from '@src/utils/createProxyForValue'; const deferredUpdatesValue = {deferredUpdates: {} as DeferredUpdatesDictionary}; diff --git a/src/libs/actions/OnyxUpdateManager/utils/index.ts b/src/libs/actions/OnyxUpdateManager/utils/index.ts index 6f45d6d64816..8fb08c505f73 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/index.ts @@ -1,6 +1,6 @@ import Onyx from 'react-native-onyx'; import * as App from '@userActions/App'; -import type DeferredUpdatesDictionary from '@userActions/OnyxUpdateManager/types'; +import type {DeferredUpdatesDictionary, DetectGapAndSplitResult} from '@userActions/OnyxUpdateManager/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {applyUpdates} from './applyUpdates'; import deferredUpdatesProxy from './deferredUpdates'; @@ -13,7 +13,7 @@ Onyx.connect({ // In order for the deferred updates to be applied correctly in order, // we need to check if there are any gaps between deferred updates. -type DetectGapAndSplitResult = {applicableUpdates: DeferredUpdatesDictionary; updatesAfterGaps: DeferredUpdatesDictionary; latestMissingUpdateID: number | undefined}; + function detectGapsAndSplit(updates: DeferredUpdatesDictionary, clientLastUpdateID?: number): DetectGapAndSplitResult { const lastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0; diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index ca7fefb4c28e..d80b4da0a8a4 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -1,12 +1,15 @@ import Onyx from 'react-native-onyx'; import * as AppImport from '@libs/actions/App'; import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; +import type {DeferredUpdatesDictionary, DetectGapAndSplitResult} from '@libs/actions/OnyxUpdateManager/types'; +import * as OnyxUpdateManagerUtilsImport from '@libs/actions/OnyxUpdateManager/utils'; import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('@libs/actions/App'); +jest.mock('@libs/actions/OnyxUpdateManager/utils'); jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); type AppActionsMock = typeof AppImport & { @@ -14,11 +17,41 @@ type AppActionsMock = typeof AppImport & { }; type ApplyUpdatesMock = typeof ApplyUpdatesImport & { - applyUpdates: jest.Mock<Promise<void[]>>; + applyUpdates: jest.Mock<Promise<[]>, [updates: DeferredUpdatesDictionary]>; +}; + +type OnyxUpdateManagerUtilsMock = typeof OnyxUpdateManagerUtilsImport & { + detectGapsAndSplit: jest.Mock<Promise<DetectGapAndSplitResult>, [updates: DeferredUpdatesDictionary, clientLastUpdateID?: number]>; + validateAndApplyDeferredUpdates: jest.Mock<Promise<void>, [clientLastUpdateID?: number]>; }; const App = AppImport as AppActionsMock; const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; +const OnyxUpdateManagerUtils = OnyxUpdateManagerUtilsImport as OnyxUpdateManagerUtilsMock; + +let onApplyUpdates: ((updates: DeferredUpdatesDictionary) => Promise<void>) | undefined; +ApplyUpdates.applyUpdates.mockImplementation((updates: DeferredUpdatesDictionary) => { + const ApplyUpdatesMock: ApplyUpdatesMock = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'); + + if (onApplyUpdates === undefined) { + return ApplyUpdatesMock.applyUpdates(updates).then(() => []); + } + + return onApplyUpdates(updates) + .then(() => ApplyUpdatesMock.applyUpdates(updates)) + .then(() => []); +}); + +let onValidateAndApplyDeferredUpdates: ((clientLastUpdateID?: number) => Promise<void>) | undefined; +OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates.mockImplementation((clientLastUpdateID?: number) => { + const UtilsMock: OnyxUpdateManagerUtilsMock = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/__mocks__'); + + if (onValidateAndApplyDeferredUpdates === undefined) { + return UtilsMock.validateAndApplyDeferredUpdates(clientLastUpdateID); + } + + return onValidateAndApplyDeferredUpdates(clientLastUpdateID).then(() => UtilsMock.validateAndApplyDeferredUpdates(clientLastUpdateID)); +}); const createMockUpdate = (lastUpdateID: number): OnyxUpdatesFromServer => ({ type: 'https', @@ -46,6 +79,8 @@ const mockUpdate3 = createMockUpdate(3); const mockUpdate4 = createMockUpdate(4); const mockUpdate5 = createMockUpdate(5); const mockUpdate6 = createMockUpdate(6); +const mockUpdate7 = createMockUpdate(7); +const mockUpdate8 = createMockUpdate(8); const resetOnyxUpdateManager = async () => { await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); @@ -78,6 +113,11 @@ describe('OnyxUpdateManager', () => { // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. expect(lastUpdateIDAppliedToClient).toBe(5); + // There are no gaps in the deferred updates, therefore only one call to getMissingOnyxUpdates should be triggered + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + // There should be only one call to applyUpdates. The call should contain all the deferred update, // since the locally applied updates have changed in the meantime. expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); @@ -86,15 +126,18 @@ describe('OnyxUpdateManager', () => { }); }); - it('should only apply deferred updates that are after the locally applied update (pending updates)', async () => { + it('should only apply deferred updates that are newer than the last locally applied update (pending updates)', async () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); - // We manually update the lastUpdateIDAppliedToClient to 5, to simulate local updates being applied, - // while we are waiting for the missing updates to be fetched. - // Only the deferred updates after the lastUpdateIDAppliedToClient should be applied. - Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); + onValidateAndApplyDeferredUpdates = async () => { + // We manually update the lastUpdateIDAppliedToClient to 5, to simulate local updates being applied, + // while we are waiting for the missing updates to be fetched. + // Only the deferred updates after the lastUpdateIDAppliedToClient should be applied. + await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); + onValidateAndApplyDeferredUpdates = undefined; + }; return waitForBatchedUpdates() .then(() => OnyxUpdateManager.queryPromise) @@ -102,6 +145,11 @@ describe('OnyxUpdateManager', () => { // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. expect(lastUpdateIDAppliedToClient).toBe(6); + // There are no gaps in the deferred updates, therefore only one call to getMissingOnyxUpdates should be triggered + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server in the first and only call to getMissingOnyxUpdates expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); @@ -125,6 +173,11 @@ describe('OnyxUpdateManager', () => { // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. expect(lastUpdateIDAppliedToClient).toBe(6); + // Even though there is a gap in the deferred updates, we only want to fetch missing updates once per batch. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); + // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); @@ -136,9 +189,125 @@ describe('OnyxUpdateManager', () => { // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap in the deferred updates. 3-4 expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); - // After the gap in the deferred updates has been resolve, the remaining deferred updates (5, 6) should be applied. + // After the gap in the deferred updates has been resolved, the remaining deferred updates (5, 6) should be applied. // eslint-disable-next-line @typescript-eslint/naming-convention expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5, 6: mockUpdate6}); }); }); + + it('should re-fetch missing deferred updates only once per batch', async () => { + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate8); + + return waitForBatchedUpdates() + .then(() => OnyxUpdateManager.queryPromise) + ?.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(8); + + // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); + + // After the initial missing updates have been applied, the applicable updates (3-4) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3, 4: mockUpdate4}); + + // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap (4-7) in the deferred updates. + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 4, 7); + + // After the gap in the deferred updates has been resolved, the remaining deferred updates (8) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {8: mockUpdate8}); + }); + }); + + it('should not re-fetch missing updates if the locally applied update has been updated', async () => { + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate7); + + onApplyUpdates = async () => { + // We manually update the lastUpdateIDAppliedToClient to 5, to simulate local updates being applied, + // while the applicable updates have been applied. + // When this happens, the OnyxUpdateManager should trigger another validation of the deferred updates, + // without triggering another re-fetching of missing updates from the server. + await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); + onApplyUpdates = undefined; + }; + + return waitForBatchedUpdates() + .then(() => OnyxUpdateManager.queryPromise) + ?.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(7); + + // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); + + // validateAndApplyDeferredUpdates should only be called once, since the locally applied update id has been updated in the meantime, + // and the deferred updates after the locally applied update don't have any gaps. + expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + + // Since there is a gap in the deferred updates, we need to run applyUpdates twice. + // Once for the applicable updates (before the gap) and then for the remaining deferred updates. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + + // After the initial missing updates have been applied, the applicable updates (3) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + + // Since the lastUpdateIDAppliedToClient has changed to 5 in the meantime, we only need to apply the remaining deferred updates (6-7). + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {6: mockUpdate6, 7: mockUpdate7}); + }); + }); + + it('should re-fetch missing updates if the locally applied update has been updated, but there are still gaps after the locally applied update', async () => { + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); + OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate7); + + onApplyUpdates = async () => { + // We manually update the lastUpdateIDAppliedToClient to 4, to simulate local updates being applied, + // while the applicable updates have been applied. + // When this happens, the OnyxUpdateManager should trigger another validation of the deferred updates, + // without triggering another re-fetching of missing updates from the server. + await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 4); + onApplyUpdates = undefined; + }; + + return waitForBatchedUpdates() + .then(() => OnyxUpdateManager.queryPromise) + ?.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(7); + + // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function, since it recursively calls itself. + expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + + // Since there is a gap in the deferred updates, we need to run applyUpdates twice. + // Once for the applicable updates (before the gap) and then for the remaining deferred updates. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + + // After the initial missing updates have been applied, the applicable updates (3) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + + // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap in the deferred updates, + // that are later than the locally applied update (4-6). (including the last locally applied update) + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 4, 6); + + // Since the lastUpdateIDAppliedToClient has changed to 4 in the meantime and we're fetching updates 5-6 we only need to apply the remaining deferred updates (7). + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {7: mockUpdate7}); + }); + }); }); From 2c50853637fc3d6909ecc65d306f8e525393c447 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Mon, 22 Apr 2024 19:53:12 +0200 Subject: [PATCH 288/580] fix: ts --- src/libs/actions/OnyxUpdateManager/utils/index.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager/utils/index.ts b/src/libs/actions/OnyxUpdateManager/utils/index.ts index 8fb08c505f73..a04c7e884219 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/index.ts @@ -57,9 +57,7 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary, clientLastUpdate // When "firstUpdateAfterGaps" is not set yet, we need to set it to the last update in the list, // because we will fetch all missing updates up to the previous one and can then always apply the last update in the deferred updates. - if (!firstUpdateAfterGaps) { - firstUpdateAfterGaps = Number(updateValues[updateValues.length - 1].lastUpdateID); - } + const firstUpdateAfterGapWithFallback = firstUpdateAfterGaps ?? Number(updateValues[updateValues.length - 1].lastUpdateID); let updatesAfterGaps: DeferredUpdatesDictionary = {}; if (gapExists) { @@ -67,7 +65,7 @@ function detectGapsAndSplit(updates: DeferredUpdatesDictionary, clientLastUpdate (accUpdates, [lastUpdateID, update]) => ({ ...accUpdates, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ...(Number(lastUpdateID) >= firstUpdateAfterGaps ? {[Number(lastUpdateID)]: update} : {}), + ...(Number(lastUpdateID) >= firstUpdateAfterGapWithFallback ? {[Number(lastUpdateID)]: update} : {}), }), {}, ); From eeee40f16665b8d89972dc6bf0ea909aa36766c3 Mon Sep 17 00:00:00 2001 From: Carlos Martins <cmartins@expensify.com> Date: Mon, 22 Apr 2024 14:08:20 -0600 Subject: [PATCH 289/580] conditionally render from --- src/components/ParentNavigationSubtitle.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index cbc9e1352f21..f793b4f8a60f 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -50,8 +50,12 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct style={[styles.optionAlternateText]} numberOfLines={1} > - <Text style={[styles.optionAlternateText, styles.textLabelSupporting]}>{`${translate('threads.from')} `}</Text> - <Text style={[styles.optionAlternateText, styles.textLabelSupporting, styles.link]}>{reportName}</Text> + {Boolean(reportName) && ( + <> + <Text style={[styles.optionAlternateText, styles.textLabelSupporting]}>{`${translate('threads.from')} `}</Text> + <Text style={[styles.optionAlternateText, styles.textLabelSupporting, styles.link]}>{reportName}</Text> + </> + )} {Boolean(workspaceName) && <Text style={[styles.optionAlternateText, styles.textLabelSupporting]}>{` ${translate('threads.in')} ${workspaceName}`}</Text>} </Text> </PressableWithoutFeedback> From de7af7f7a6b870c23a571129b4d9142fbf5dfc51 Mon Sep 17 00:00:00 2001 From: Qichen Zhu <57348009+QichenZhu@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:03:44 +1200 Subject: [PATCH 290/580] fix adding a reaction on a highlighted message moves up and down strangely --- src/styles/utils/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index e9efc84e8807..52e4f10178f9 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1452,6 +1452,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ ...positioning.r4, ...styles.cursorDefault, ...styles.userSelectNone, + overflowAnchor: 'none', position: 'absolute', zIndex: 8, }), From 5579af264184196079abb3089f75d3e82ebc606c Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Tue, 23 Apr 2024 09:36:29 +0200 Subject: [PATCH 291/580] TS fixes --- src/components/ReportWelcomeText.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 8ebf5f3ced91..b9eeee63ce4c 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -2,7 +2,6 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -11,6 +10,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; +import type {IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; @@ -47,7 +47,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, policy, participantAccountIDs, canUseTrackExpense); const additionalText = moneyRequestOptions - .filter((item): item is ValueOf<Omit<typeof CONST.IOU.TYPE, 'INVOICE'>> => item !== CONST.IOU.TYPE.INVOICE) + .filter((item): item is Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.INVOICE> => item !== CONST.IOU.TYPE.INVOICE) .map((item) => translate(`reportActionsView.iouTypes.${item}`)) .join(', '); const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy); From 5801e4f56cbba6560072c56e1e64c52540dd0df5 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Tue, 23 Apr 2024 09:44:04 +0200 Subject: [PATCH 292/580] Lint fixes --- src/libs/IOUUtils.ts | 10 +++++++++- .../iou/request/step/IOURequestStepConfirmation.tsx | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index d128e72ade5a..90e54d8b1a8c 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -109,7 +109,15 @@ function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { * Checks if the iou type is one of request, send, invoice or split. */ function isValidMoneyRequestType(iouType: string): boolean { - const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SUBMIT, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE]; + const moneyRequestType: string[] = [ + CONST.IOU.TYPE.REQUEST, + CONST.IOU.TYPE.SUBMIT, + CONST.IOU.TYPE.SPLIT, + CONST.IOU.TYPE.SEND, + CONST.IOU.TYPE.PAY, + CONST.IOU.TYPE.TRACK, + CONST.IOU.TYPE.INVOICE, + ]; return moneyRequestType.includes(iouType); } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 6e9d52656924..a2b0d48678ed 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -512,7 +512,9 @@ function IOURequestStepConfirmation({ <HeaderWithBackButton title={headerTitle} onBackButtonPress={navigateBack} - shouldShowThreeDotsButton={requestType === CONST.IOU.REQUEST_TYPE.MANUAL && (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.TRACK || iouType === CONST.IOU.TYPE.INVOICE)} + shouldShowThreeDotsButton={ + requestType === CONST.IOU.REQUEST_TYPE.MANUAL && (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.TRACK || iouType === CONST.IOU.TYPE.INVOICE) + } threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} threeDotsMenuItems={[ { From ed7a5fe11e7f33dc27e3f9d661231bd4059df5bd Mon Sep 17 00:00:00 2001 From: Monil Bhavsar <monil@expensify.com> Date: Tue, 23 Apr 2024 13:51:58 +0530 Subject: [PATCH 293/580] Refactor code --- src/libs/actions/Report.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3e83281dc83f..742e5fa296ea 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2466,12 +2466,13 @@ function navigateToMostRecentReport(currentReport: OnyxEntry<Report>) { const lastAccessedReportID = filteredReportsByLastRead.at(-1)?.reportID; const isChatThread = ReportUtils.isChatThread(currentReport); if (lastAccessedReportID) { + const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? ''); // If it is not a chat thread we should call Navigation.goBack to pop the current route first before navigating to last accessed report. if (!isChatThread) { // Fallback to the lastAccessedReportID route, if this is first route in the navigator - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? '')); + Navigation.goBack(lastAccessedReportRoute); } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID ?? ''), CONST.NAVIGATION.TYPE.FORCED_UP); + Navigation.navigate(lastAccessedReportRoute, CONST.NAVIGATION.TYPE.FORCED_UP); } else { const participantAccountIDs = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE]); const chat = ReportUtils.getChatByParticipants(participantAccountIDs); From 0997b0a4eaef675cb535abef7188f980d3bb23cc Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Tue, 23 Apr 2024 15:31:55 +0700 Subject: [PATCH 294/580] Use more reasonable variable names --- src/components/ButtonWithDropdownMenu/index.tsx | 14 +++++++------- src/components/ButtonWithDropdownMenu/types.ts | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 2 +- .../categories/WorkspaceCategoriesPage.tsx | 2 +- .../distanceRates/PolicyDistanceRatesPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 +- src/pages/workspace/taxes/WorkspaceTaxesPage.tsx | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 16c9804f79d0..1ac73844509b 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -14,7 +14,7 @@ import type {ButtonWithDropdownMenuProps} from './types'; function ButtonWithDropdownMenu<IValueType>({ success = false, - isSplit = true, + isSplitButton = true, isLoading = false, isDisabled = false, pressOnEnter = false, @@ -66,7 +66,7 @@ function ButtonWithDropdownMenu<IValueType>({ } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); - const getIconRightButton = useCallback( + const getIconRight = useCallback( () => ( <Icon medium={isButtonSizeLarge} @@ -88,7 +88,7 @@ function ButtonWithDropdownMenu<IValueType>({ ref={(ref) => { caretButton.current = ref; }} - onPress={(event) => (!isSplit ? setIsMenuVisible(!isMenuVisible) : onPress(event, selectedItem.value))} + onPress={(event) => (!isSplitButton ? setIsMenuVisible(!isMenuVisible) : onPress(event, selectedItem.value))} text={customText ?? selectedItem.text} isDisabled={isDisabled || !!selectedItem.disabled} isLoading={isLoading} @@ -96,13 +96,13 @@ function ButtonWithDropdownMenu<IValueType>({ style={[styles.flex1, styles.pr0]} large={isButtonSizeLarge} medium={!isButtonSizeLarge} - innerStyles={[innerStyleDropButton, !isSplit && styles.dropDownButtonCartIconView]} + innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} - customRightIcon={getIconRightButton()} - shouldShowRightIcon={!isSplit} + customRightIcon={getIconRight()} + shouldShowRightIcon={!isSplitButton} /> - {isSplit && ( + {isSplitButton && ( <Button ref={caretButton} success={success} diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index 1b4533087e44..1ad2ccb0d717 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -76,7 +76,7 @@ type ButtonWithDropdownMenuProps<TValueType> = { wrapperStyle?: StyleProp<ViewStyle>; /** Whether the button should use split style or not */ - isSplit?: boolean; + isSplitButton?: boolean; }; export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps, WorkspaceTaxRatesBulkActionType}; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index f1107a1a2b8f..e8464e4498d5 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -516,7 +516,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, onPress={() => null} options={getBulkActionsButtonOptions()} buttonRef={dropdownButtonRef} - isSplit={false} + isSplitButton={false} style={[isSmallScreenWidth && styles.flexGrow1]} /> ) : ( diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 8c91a743e3c7..b0eb61b0d9d6 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -215,7 +215,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} customText={translate('workspace.common.selected', {selectedNumber: selectedCategoriesArray.length})} options={options} - isSplit={false} + isSplitButton={false} style={[isSmallScreenWidth && styles.flexGrow1, isSmallScreenWidth && styles.mb3]} /> ); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 031e3f456db1..8e1eeb7b66f4 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -260,7 +260,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) buttonRef={dropdownButtonRef} style={[isSmallScreenWidth && styles.flexGrow1]} wrapperStyle={styles.w100} - isSplit={false} + isSplitButton={false} /> )} </View> diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index d5ade4c3fc6c..66cd5b07f3cc 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -225,7 +225,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { onPress={() => null} shouldAlwaysShowDropdownMenu pressOnEnter - isSplit={false} + isSplitButton={false} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} customText={translate('workspace.common.selected', {selectedNumber: selectedTagsArray.length})} options={options} diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index 3c5e790cfbca..65a332868da4 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -233,7 +233,7 @@ function WorkspaceTaxesPage({ customText={translate('workspace.common.selected', {selectedNumber: selectedTaxesIDs.length})} shouldAlwaysShowDropdownMenu pressOnEnter - isSplit={false} + isSplitButton={false} style={[isSmallScreenWidth && styles.w50, isSmallScreenWidth && styles.mb3]} /> ); From 3904ef4a167ca630dd5c793afbc61431d06def20 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 11:20:08 +0200 Subject: [PATCH 295/580] fix: tests --- tests/unit/OnyxUpdateManagerTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index d80b4da0a8a4..2110f8f49d67 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -176,7 +176,7 @@ describe('OnyxUpdateManager', () => { // Even though there is a gap in the deferred updates, we only want to fetch missing updates once per batch. expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); - expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. @@ -210,7 +210,7 @@ describe('OnyxUpdateManager', () => { // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); - expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); // After the initial missing updates have been applied, the applicable updates (3-4) should be applied. // eslint-disable-next-line @typescript-eslint/naming-convention @@ -291,7 +291,7 @@ describe('OnyxUpdateManager', () => { // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. // Unfortunately, we cannot easily count the calls of this function, since it recursively calls itself. - expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); // Since there is a gap in the deferred updates, we need to run applyUpdates twice. // Once for the applicable updates (before the gap) and then for the remaining deferred updates. From abff1aabc8ab9ce946ac11f16b240feddbec5cd2 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 12:15:25 +0200 Subject: [PATCH 296/580] move specific mocks to mock folders --- .../utils/__mocks__/applyUpdates.ts | 24 +++++++-- .../utils/__mocks__/index.ts | 35 +++++++++--- src/libs/actions/__mocks__/App.ts | 5 ++ tests/unit/OnyxUpdateManagerTest.ts | 53 ++++--------------- 4 files changed, 61 insertions(+), 56 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts index 412214cdf2ec..d02a034c29cc 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import createProxyForValue from '@src/utils/createProxyForValue'; let lastUpdateIDAppliedToClient = 0; Onyx.connect({ @@ -8,11 +9,26 @@ Onyx.connect({ callback: (value) => (lastUpdateIDAppliedToClient = value ?? 0), }); +type ApplyUpdatesMockValues = { + onApplyUpdates: ((updates: DeferredUpdatesDictionary) => Promise<void>) | undefined; +}; + +type ApplyUpdatesMock = { + applyUpdates: jest.Mock<Promise<[]>, [updates: DeferredUpdatesDictionary]>; + mockValues: ApplyUpdatesMockValues; +}; + +const mockValues: ApplyUpdatesMockValues = { + onApplyUpdates: undefined, +}; +const mockValuesProxy = createProxyForValue(mockValues); + const applyUpdates = jest.fn((updates: DeferredUpdatesDictionary) => { const lastUpdateIdFromUpdates = Math.max(...Object.keys(updates).map(Number)); - const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Math.max(lastUpdateIDAppliedToClient, lastUpdateIdFromUpdates)); - return promise; + return (mockValuesProxy.onApplyUpdates === undefined ? Promise.resolve() : mockValuesProxy.onApplyUpdates(updates)).then(() => + Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Math.max(lastUpdateIDAppliedToClient, lastUpdateIdFromUpdates)), + ); }); -// eslint-disable-next-line import/prefer-default-export -export {applyUpdates}; +export {applyUpdates, mockValuesProxy as mockValues}; +export type {ApplyUpdatesMock}; diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts index 3cfb0abefbeb..461e074dd881 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts @@ -1,12 +1,31 @@ -import type * as UtilsImport from '..'; +import type {DeferredUpdatesDictionary, DetectGapAndSplitResult} from '@libs/actions/OnyxUpdateManager/types'; +import createProxyForValue from '@src/utils/createProxyForValue'; +import type * as OnyxUpdateManagerUtilsImport from '..'; -const UtilsImplementation: typeof UtilsImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils'); +const UtilsImplementation: typeof OnyxUpdateManagerUtilsImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils'); -const detectGapsAndSplit = jest.fn(UtilsImplementation.detectGapsAndSplit); -const validateAndApplyDeferredUpdates = jest.fn(UtilsImplementation.validateAndApplyDeferredUpdates); +type OnyxUpdateManagerUtilsMockValues = { + onValidateAndApplyDeferredUpdates: ((clientLastUpdateID?: number) => Promise<void>) | undefined; +}; + +type OnyxUpdateManagerUtilsMock = typeof UtilsImplementation & { + detectGapsAndSplit: jest.Mock<Promise<DetectGapAndSplitResult>, [updates: DeferredUpdatesDictionary, clientLastUpdateID?: number]>; + validateAndApplyDeferredUpdates: jest.Mock<Promise<void>, [clientLastUpdateID?: number]>; + mockValues: OnyxUpdateManagerUtilsMockValues; +}; -export { - // Mocks - detectGapsAndSplit, - validateAndApplyDeferredUpdates, +const mockValues: OnyxUpdateManagerUtilsMockValues = { + onValidateAndApplyDeferredUpdates: undefined, }; +const mockValuesProxy = createProxyForValue(mockValues); + +const detectGapsAndSplit = jest.fn(UtilsImplementation.detectGapsAndSplit); + +const validateAndApplyDeferredUpdates = jest.fn((clientLastUpdateID?: number) => + (mockValuesProxy.onValidateAndApplyDeferredUpdates === undefined ? Promise.resolve() : mockValuesProxy.onValidateAndApplyDeferredUpdates(clientLastUpdateID)).then(() => + UtilsImplementation.validateAndApplyDeferredUpdates(clientLastUpdateID), + ), +); + +export {detectGapsAndSplit, validateAndApplyDeferredUpdates, mockValuesProxy as mockValues}; +export type {OnyxUpdateManagerUtilsMock}; diff --git a/src/libs/actions/__mocks__/App.ts b/src/libs/actions/__mocks__/App.ts index 6b834cf4f371..3de0c3d946c9 100644 --- a/src/libs/actions/__mocks__/App.ts +++ b/src/libs/actions/__mocks__/App.ts @@ -23,6 +23,10 @@ const { KEYS_TO_PRESERVE, } = AppImplementation; +type AppActionsMock = typeof AppImport & { + getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; +}; + const getMissingOnyxUpdates = jest.fn((_fromID: number, toID: number) => { const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, toID); return promise; @@ -51,3 +55,4 @@ export { updateLastVisitedPath, KEYS_TO_PRESERVE, }; +export type {AppActionsMock}; diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 2110f8f49d67..57884fa8fd7c 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -1,8 +1,10 @@ import Onyx from 'react-native-onyx'; +import type {AppActionsMock} from '@libs/actions/__mocks__/App'; import * as AppImport from '@libs/actions/App'; import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; -import type {DeferredUpdatesDictionary, DetectGapAndSplitResult} from '@libs/actions/OnyxUpdateManager/types'; import * as OnyxUpdateManagerUtilsImport from '@libs/actions/OnyxUpdateManager/utils'; +import type {OnyxUpdateManagerUtilsMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__'; +import type {ApplyUpdatesMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'; import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; @@ -12,47 +14,10 @@ jest.mock('@libs/actions/App'); jest.mock('@libs/actions/OnyxUpdateManager/utils'); jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); -type AppActionsMock = typeof AppImport & { - getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; -}; - -type ApplyUpdatesMock = typeof ApplyUpdatesImport & { - applyUpdates: jest.Mock<Promise<[]>, [updates: DeferredUpdatesDictionary]>; -}; - -type OnyxUpdateManagerUtilsMock = typeof OnyxUpdateManagerUtilsImport & { - detectGapsAndSplit: jest.Mock<Promise<DetectGapAndSplitResult>, [updates: DeferredUpdatesDictionary, clientLastUpdateID?: number]>; - validateAndApplyDeferredUpdates: jest.Mock<Promise<void>, [clientLastUpdateID?: number]>; -}; - const App = AppImport as AppActionsMock; const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; const OnyxUpdateManagerUtils = OnyxUpdateManagerUtilsImport as OnyxUpdateManagerUtilsMock; -let onApplyUpdates: ((updates: DeferredUpdatesDictionary) => Promise<void>) | undefined; -ApplyUpdates.applyUpdates.mockImplementation((updates: DeferredUpdatesDictionary) => { - const ApplyUpdatesMock: ApplyUpdatesMock = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'); - - if (onApplyUpdates === undefined) { - return ApplyUpdatesMock.applyUpdates(updates).then(() => []); - } - - return onApplyUpdates(updates) - .then(() => ApplyUpdatesMock.applyUpdates(updates)) - .then(() => []); -}); - -let onValidateAndApplyDeferredUpdates: ((clientLastUpdateID?: number) => Promise<void>) | undefined; -OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates.mockImplementation((clientLastUpdateID?: number) => { - const UtilsMock: OnyxUpdateManagerUtilsMock = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/__mocks__'); - - if (onValidateAndApplyDeferredUpdates === undefined) { - return UtilsMock.validateAndApplyDeferredUpdates(clientLastUpdateID); - } - - return onValidateAndApplyDeferredUpdates(clientLastUpdateID).then(() => UtilsMock.validateAndApplyDeferredUpdates(clientLastUpdateID)); -}); - const createMockUpdate = (lastUpdateID: number): OnyxUpdatesFromServer => ({ type: 'https', lastUpdateID, @@ -131,12 +96,12 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); - onValidateAndApplyDeferredUpdates = async () => { + OnyxUpdateManagerUtils.mockValues.onValidateAndApplyDeferredUpdates = async () => { // We manually update the lastUpdateIDAppliedToClient to 5, to simulate local updates being applied, // while we are waiting for the missing updates to be fetched. // Only the deferred updates after the lastUpdateIDAppliedToClient should be applied. await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); - onValidateAndApplyDeferredUpdates = undefined; + OnyxUpdateManagerUtils.mockValues.onValidateAndApplyDeferredUpdates = undefined; }; return waitForBatchedUpdates() @@ -231,13 +196,13 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate7); - onApplyUpdates = async () => { + ApplyUpdates.mockValues.onApplyUpdates = async () => { // We manually update the lastUpdateIDAppliedToClient to 5, to simulate local updates being applied, // while the applicable updates have been applied. // When this happens, the OnyxUpdateManager should trigger another validation of the deferred updates, // without triggering another re-fetching of missing updates from the server. await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 5); - onApplyUpdates = undefined; + ApplyUpdates.mockValues.onApplyUpdates = undefined; }; return waitForBatchedUpdates() @@ -271,13 +236,13 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate7); - onApplyUpdates = async () => { + ApplyUpdates.mockValues.onApplyUpdates = async () => { // We manually update the lastUpdateIDAppliedToClient to 4, to simulate local updates being applied, // while the applicable updates have been applied. // When this happens, the OnyxUpdateManager should trigger another validation of the deferred updates, // without triggering another re-fetching of missing updates from the server. await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 4); - onApplyUpdates = undefined; + ApplyUpdates.mockValues.onApplyUpdates = undefined; }; return waitForBatchedUpdates() From 39387f64b8b4b08a98eaf275423edcc4396f1ac9 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 12:29:05 +0200 Subject: [PATCH 297/580] fix: update mocks and tests --- .../utils/__mocks__/index.ts | 3 +- tests/unit/OnyxUpdateManagerTest.ts | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts index 461e074dd881..e548a9e22b1a 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts @@ -1,6 +1,7 @@ import type {DeferredUpdatesDictionary, DetectGapAndSplitResult} from '@libs/actions/OnyxUpdateManager/types'; import createProxyForValue from '@src/utils/createProxyForValue'; import type * as OnyxUpdateManagerUtilsImport from '..'; +import {applyUpdates} from './applyUpdates'; const UtilsImplementation: typeof OnyxUpdateManagerUtilsImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils'); @@ -27,5 +28,5 @@ const validateAndApplyDeferredUpdates = jest.fn((clientLastUpdateID?: number) => ), ); -export {detectGapsAndSplit, validateAndApplyDeferredUpdates, mockValuesProxy as mockValues}; +export {applyUpdates, detectGapsAndSplit, validateAndApplyDeferredUpdates, mockValuesProxy as mockValues}; export type {OnyxUpdateManagerUtilsMock}; diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 57884fa8fd7c..b7b6e103fbde 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -81,7 +81,11 @@ describe('OnyxUpdateManager', () => { // There are no gaps in the deferred updates, therefore only one call to getMissingOnyxUpdates should be triggered expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); - expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); // There should be only one call to applyUpdates. The call should contain all the deferred update, // since the locally applied updates have changed in the meantime. @@ -113,7 +117,11 @@ describe('OnyxUpdateManager', () => { // There are no gaps in the deferred updates, therefore only one call to getMissingOnyxUpdates should be triggered expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); - expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server in the first and only call to getMissingOnyxUpdates expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); @@ -141,6 +149,10 @@ describe('OnyxUpdateManager', () => { // Even though there is a gap in the deferred updates, we only want to fetch missing updates once per batch. expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. @@ -175,6 +187,9 @@ describe('OnyxUpdateManager', () => { // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); // After the initial missing updates have been applied, the applicable updates (3-4) should be applied. @@ -214,9 +229,10 @@ describe('OnyxUpdateManager', () => { // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); - // validateAndApplyDeferredUpdates should only be called once, since the locally applied update id has been updated in the meantime, - // and the deferred updates after the locally applied update don't have any gaps. - expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); // Since there is a gap in the deferred updates, we need to run applyUpdates twice. // Once for the applicable updates (before the gap) and then for the remaining deferred updates. @@ -255,7 +271,8 @@ describe('OnyxUpdateManager', () => { expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. - // Unfortunately, we cannot easily count the calls of this function, since it recursively calls itself. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); // Since there is a gap in the deferred updates, we need to run applyUpdates twice. From 63279686e75b5072f3e08dd9f35bf44a77017438 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 12:33:36 +0200 Subject: [PATCH 298/580] simplify --- tests/unit/OnyxUpdateManagerTest.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index b7b6e103fbde..5cb636e3a572 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -47,11 +47,6 @@ const mockUpdate6 = createMockUpdate(6); const mockUpdate7 = createMockUpdate(7); const mockUpdate8 = createMockUpdate(8); -const resetOnyxUpdateManager = async () => { - await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); - OnyxUpdateManager.resetDeferralLogicVariables(); -}; - describe('OnyxUpdateManager', () => { let lastUpdateIDAppliedToClient = 1; beforeAll(() => { @@ -63,7 +58,8 @@ describe('OnyxUpdateManager', () => { beforeEach(async () => { jest.clearAllMocks(); - await resetOnyxUpdateManager(); + await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); + OnyxUpdateManager.resetDeferralLogicVariables(); return waitForBatchedUpdates(); }); From ff600c740ba8956865a69d81ba96857d437845ad Mon Sep 17 00:00:00 2001 From: war-in <war-in@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:09:25 +0200 Subject: [PATCH 299/580] update expensify-common --- package-lock.json | 6 +++--- package.json | 2 +- src/pages/workspace/WorkspaceNewRoomPage.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8cab4c43db4f..cbaede5c0a48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com:software-mansion-labs/expensify-common.git#7596ae07c9cd5a6b5265897034898b4526d681dc", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", @@ -20209,8 +20209,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/software-mansion-labs/expensify-common.git#7596ae07c9cd5a6b5265897034898b4526d681dc", - "integrity": "sha512-Ns7qkMuJ4SeLj0lrj3i+KqHBzjlym8baDlS7CUIqq2tuNXkgxwO4D+5d6U3ooLOf0CyWb56KaGy5TOTFqpJDZA==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", + "integrity": "sha512-9BHjM3kZs7/dil0oykEQFkEhXjVD5liTttmO7ZYtPZkl4j6g97mubY2p9lYpWwpkWckUfvU7nGuZQjahw9xSFA==", "license": "MIT", "dependencies": { "classnames": "2.5.0", diff --git a/package.json b/package.json index 590bc1c9af79..2d09680fb69c 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com:software-mansion-labs/expensify-common.git#7596ae07c9cd5a6b5265897034898b4526d681dc", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#9a68635cdcef4c81593c0f816a007bc9c707d46a", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.11.0", diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index ffd8cc478ef4..9a204755a1e5 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -197,7 +197,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli return errors; }, - [reports], + [reports, policyID], ); const writeCapabilityOptions = useMemo( From 3af842806d1dcf6c96af9b7d81b88960d575886f Mon Sep 17 00:00:00 2001 From: war-in <war-in@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:27:35 +0200 Subject: [PATCH 300/580] introduce ParsingDetails object --- src/libs/ReportUtils.ts | 26 ++++++++++++------- src/libs/actions/Policy.ts | 2 +- src/libs/actions/Report.ts | 4 +-- .../ComposerWithSuggestions.tsx | 2 +- .../report/ReportActionItemMessageEdit.tsx | 2 +- src/pages/workspace/WorkspaceNewRoomPage.tsx | 4 +-- 6 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3df0194f6e5a..ac259863489b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -467,6 +467,12 @@ type OutstandingChildRequest = { hasOutstandingChildRequest?: boolean; }; +type ParsingDetails = { + shouldEscapeText?: boolean; + reportID?: string; + policyID?: string; +}; + let currentUserEmail: string | undefined; let currentUserPrivateDomain: string | undefined; let currentUserAccountID: number | undefined; @@ -3161,14 +3167,14 @@ function addDomainToShortMention(mention: string): string | undefined { * For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database * For longer comments, skip parsing, but still escape the text, and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!! */ -function getParsedComment(text: string, shouldEscapeText?: boolean, currentReportID?: string, policyID?: string): string { +function getParsedComment(text: string, parsingDetails?: ParsingDetails): string { let isGroupPolicyReport = false; - if (currentReportID) { - const currentReport = getReport(currentReportID); + if (parsingDetails?.reportID) { + const currentReport = getReport(parsingDetails?.reportID); isGroupPolicyReport = currentReport && !isEmptyObject(currentReport) ? isGroupPolicy(currentReport) : false; } - if (policyID) { - const policyType = getPolicy(policyID).type; + if (parsingDetails?.policyID) { + const policyType = getPolicy(parsingDetails?.policyID).type; isGroupPolicyReport = policyType === CONST.POLICY.TYPE.CORPORATE || policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.FREE; } @@ -3179,7 +3185,9 @@ function getParsedComment(text: string, shouldEscapeText?: boolean, currentRepor return mentionWithDomain ? `@${mentionWithDomain}` : match; }); - return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(textWithMention, {shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']}) : lodashEscape(text); + return text.length <= CONST.MAX_MARKUP_LENGTH + ? parser.replace(textWithMention, {shouldEscapeText: parsingDetails?.shouldEscapeText, disabledRules: isGroupPolicyReport ? [] : ['reportMentions']}) + : lodashEscape(text); } function getReportDescriptionText(report: Report): string { @@ -3209,7 +3217,7 @@ function buildOptimisticAddCommentReportAction( reportID?: string, ): OptimisticReportAction { const parser = new ExpensiMark(); - const commentText = getParsedComment(text ?? '', shouldEscapeText, reportID); + const commentText = getParsedComment(text ?? '', {shouldEscapeText, reportID}); const isAttachmentOnly = file && !text; const isTextOnly = text && !file; @@ -4931,8 +4939,8 @@ function getNewMarkerReportActionID(report: OnyxEntry<Report>, sortedAndFiltered * Used for compatibility with the backend auth validator for AddComment, and to account for MD in comments * @returns The comment's total length as seen from the backend */ -function getCommentLength(textComment: string, reportID?: string, policyID?: string): number { - return getParsedComment(textComment, undefined, reportID, policyID) +function getCommentLength(textComment: string, parsingDetails?: ParsingDetails): number { + return getParsedComment(textComment, parsingDetails) .replace(/[^ -~]/g, '\\u????') .trim().length; } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index f2ba65bf0717..39648814b143 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1722,7 +1722,7 @@ function updateWorkspaceDescription(policyID: string, description: string, curre if (description === currentDescription) { return; } - const parsedDescription = ReportUtils.getParsedComment(description, undefined, undefined, policyID); + const parsedDescription = ReportUtils.getParsedComment(description, {policyID}); const optimisticData: OnyxUpdate[] = [ { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index f062a4d78c68..e946a1264a7d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -461,7 +461,7 @@ function addActions(reportID: string, text = '', file?: FileObject) { if (text && file) { // When there is both text and a file, the text for the report comment needs to be parsed) - reportCommentText = ReportUtils.getParsedComment(text ?? '', undefined, reportID); + reportCommentText = ReportUtils.getParsedComment(text ?? '', {reportID}); // And the API command needs to go to the new API which supports combining both text and attachments in a single report action commandName = WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT; @@ -1848,7 +1848,7 @@ function updateDescription(reportID: string, previousValue: string, newValue: st return; } - const parsedDescription = ReportUtils.getParsedComment(newValue, undefined, reportID); + const parsedDescription = ReportUtils.getParsedComment(newValue, {reportID}); const optimisticData: OnyxUpdate[] = [ { diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 4be8db304b92..f33e9a1a9de4 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -474,7 +474,7 @@ function ComposerWithSuggestions( const prepareCommentAndResetComposer = useCallback((): string => { const trimmedComment = commentRef.current.trim(); - const commentLength = ReportUtils.getCommentLength(trimmedComment, reportID); + const commentLength = ReportUtils.getCommentLength(trimmedComment, {reportID}); // Don't submit empty comments or comments that exceed the character limit if (!commentLength || commentLength > CONST.MAX_COMMENT_LENGTH) { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 79a22f61e17b..bfd2c8b5ca7f 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -318,7 +318,7 @@ function ReportActionItemMessageEdit( */ const publishDraft = useCallback(() => { // Do nothing if draft exceed the character limit - if (ReportUtils.getCommentLength(draft, reportID) > CONST.MAX_COMMENT_LENGTH) { + if (ReportUtils.getCommentLength(draft, {reportID}) > CONST.MAX_COMMENT_LENGTH) { return; } diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 9a204755a1e5..c66506d4a9b1 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -105,7 +105,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli */ const submit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.NEW_ROOM_FORM>) => { const participants = [session?.accountID ?? 0]; - const parsedDescription = ReportUtils.getParsedComment(values.reportDescription ?? '', undefined, undefined, policyID); + const parsedDescription = ReportUtils.getParsedComment(values.reportDescription ?? '', {policyID}); const policyReport = ReportUtils.buildOptimisticChatReport( participants, values.roomName, @@ -183,7 +183,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } - const descriptionLength = ReportUtils.getCommentLength(values.reportDescription, policyID); + const descriptionLength = ReportUtils.getCommentLength(values.reportDescription, {policyID}); if (descriptionLength > CONST.REPORT_DESCRIPTION.MAX_LENGTH) { ErrorUtils.addErrorMessage(errors, 'reportDescription', [ 'common.error.characterLimitExceedCounter', From b0a70babbfc10787742d742e9c736e43b3e8d74c Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 15:27:43 +0200 Subject: [PATCH 301/580] fix: update mocks, tests and test utils to allow for broader E2E tests --- src/libs/actions/OnyxUpdateManager/index.ts | 15 ++- src/libs/actions/__mocks__/App.ts | 24 +++- tests/actions/OnyxUpdateManagerTest.ts | 131 ++++++++++++++++++++ tests/unit/OnyxUpdateManagerTest.ts | 36 ++---- tests/utils/createOnyxMockUpdate.ts | 23 ++++ 5 files changed, 195 insertions(+), 34 deletions(-) create mode 100644 tests/actions/OnyxUpdateManagerTest.ts create mode 100644 tests/utils/createOnyxMockUpdate.ts diff --git a/src/libs/actions/OnyxUpdateManager/index.ts b/src/libs/actions/OnyxUpdateManager/index.ts index d46f31fe952b..8c6695379614 100644 --- a/src/libs/actions/OnyxUpdateManager/index.ts +++ b/src/libs/actions/OnyxUpdateManager/index.ts @@ -39,9 +39,16 @@ Onyx.connect({ }, }); -// eslint-disable-next-line import/no-mutable-exports let queryPromise: Promise<Response | Response[] | void> | undefined; +let resolveQueryPromiseWrapper: () => void; +const createQueryPromiseWrapper = () => + new Promise<void>((resolve) => { + resolveQueryPromiseWrapper = resolve; + }); +// eslint-disable-next-line import/no-mutable-exports +let queryPromiseWrapper = createQueryPromiseWrapper(); + const resetDeferralLogicVariables = () => { queryPromise = undefined; deferredUpdatesProxy.deferredUpdates = {}; @@ -50,6 +57,10 @@ const resetDeferralLogicVariables = () => { // This function will reset the query variables, unpause the SequentialQueue and log an info to the user. function finalizeUpdatesAndResumeQueue() { console.debug('[OnyxUpdateManager] Done applying all updates'); + + resolveQueryPromiseWrapper(); + queryPromiseWrapper = createQueryPromiseWrapper(); + resetDeferralLogicVariables(); Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); SequentialQueue.unpause(); @@ -145,4 +156,4 @@ export default () => { }); }; -export {handleOnyxUpdateGap, queryPromise, resetDeferralLogicVariables}; +export {handleOnyxUpdateGap, queryPromiseWrapper as queryPromise, resetDeferralLogicVariables}; diff --git a/src/libs/actions/__mocks__/App.ts b/src/libs/actions/__mocks__/App.ts index 3de0c3d946c9..574a5b51e1b9 100644 --- a/src/libs/actions/__mocks__/App.ts +++ b/src/libs/actions/__mocks__/App.ts @@ -1,6 +1,9 @@ import Onyx from 'react-native-onyx'; import type * as AppImport from '@libs/actions/App'; +import type * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; +import createProxyForValue from '@src/utils/createProxyForValue'; const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); const { @@ -23,18 +26,33 @@ const { KEYS_TO_PRESERVE, } = AppImplementation; +type AppMockValues = { + missingOnyxUpdatesToBeApplied: OnyxUpdatesFromServer[] | undefined; +}; + type AppActionsMock = typeof AppImport & { - getMissingOnyxUpdates: jest.Mock<Promise<void[]>>; + getMissingOnyxUpdates: jest.Mock<Promise<Response[] | void[]>>; + mockValues: AppMockValues; }; +const mockValues: AppMockValues = { + missingOnyxUpdatesToBeApplied: undefined, +}; +const mockValuesProxy = createProxyForValue(mockValues); + +const ApplyUpdatesImplementation: typeof ApplyUpdatesImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); const getMissingOnyxUpdates = jest.fn((_fromID: number, toID: number) => { - const promise = Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, toID); - return promise; + if (mockValuesProxy.missingOnyxUpdatesToBeApplied === undefined) { + return Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, toID); + } + + return ApplyUpdatesImplementation.applyUpdates(mockValuesProxy.missingOnyxUpdatesToBeApplied); }); export { // Mocks getMissingOnyxUpdates, + mockValuesProxy as mockValues, // Actual App implementation setLocale, diff --git a/tests/actions/OnyxUpdateManagerTest.ts b/tests/actions/OnyxUpdateManagerTest.ts new file mode 100644 index 000000000000..e51285879e3b --- /dev/null +++ b/tests/actions/OnyxUpdateManagerTest.ts @@ -0,0 +1,131 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; +import type {AppActionsMock} from '@libs/actions/__mocks__/App'; +import * as AppImport from '@libs/actions/App'; +import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably'; +import * as OnyxUpdateManagerExports from '@libs/actions/OnyxUpdateManager'; +import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; +import type {ApplyUpdatesMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'; +import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import createOnyxMockUpdate from '../utils/createOnyxMockUpdate'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('@libs/actions/App'); +jest.mock('@libs/actions/OnyxUpdateManager/utils'); +jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates', () => { + const ApplyUpdatesImplementation: typeof ApplyUpdatesImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); + + return { + applyUpdates: jest.fn((updates: DeferredUpdatesDictionary) => ApplyUpdatesImplementation.applyUpdates(updates)), + }; +}); + +const App = AppImport as AppActionsMock; +const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; + +const TEST_USER_ACCOUNT_ID = 1; +const REPORT_ID = 'testReport1'; +const ONYX_KEY = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const; + +const exampleReportAction: Partial<OnyxTypes.ReportAction> = { + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + actorAccountID: TEST_USER_ACCOUNT_ID, + automatic: false, + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', + message: [{type: 'COMMENT', html: 'Testing a comment', text: 'Testing a comment', translationKey: ''}], + person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], + shouldShow: true, +}; + +const initialData = {report1: exampleReportAction, report2: exampleReportAction, report3: exampleReportAction} as OnyxTypes.ReportActions; + +const mockUpdate2 = createOnyxMockUpdate(2, [ + { + onyxMethod: OnyxUtils.METHOD.MERGE, + key: ONYX_KEY, + value: { + report1: null, + }, + }, +]); + +const report2PersonDiff = [ + {type: 'TEXT', style: 'light', text: 'Other Test User'}, + {type: 'TEXT', style: 'light', text: 'Other Test User 2'}, +]; +const mockUpdate3 = createOnyxMockUpdate(3, [ + { + onyxMethod: OnyxUtils.METHOD.MERGE, + key: ONYX_KEY, + value: { + report2: { + person: report2PersonDiff, + }, + report3: { + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_5.png', + }, + }, + }, +]); +const mockUpdate4 = createOnyxMockUpdate(4, [ + { + onyxMethod: OnyxUtils.METHOD.MERGE, + key: ONYX_KEY, + value: { + report3: null, + }, + }, +]); + +OnyxUpdateManager(); + +describe('actions/OnyxUpdateManager', () => { + let reportActions: OnyxEntry<OnyxTypes.ReportActions>; + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + Onyx.connect({ + key: ONYX_KEY, + callback: (val) => (reportActions = val), + }); + }); + + beforeEach(async () => { + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); + jest.clearAllMocks(); + await Onyx.clear(); + await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); + await Onyx.set(ONYX_KEY, initialData); + + App.mockValues.missingOnyxUpdatesToBeApplied = undefined; + OnyxUpdateManagerExports.resetDeferralLogicVariables(); + return waitForBatchedUpdates(); + }); + + it('should trigger Onyx update gap handling', async () => { + App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate3]; + + applyOnyxUpdatesReliably(mockUpdate2); + applyOnyxUpdatesReliably(mockUpdate4); + applyOnyxUpdatesReliably(mockUpdate3); + + return OnyxUpdateManagerExports.queryPromise.then(() => { + const expectedResult: Record<string, Partial<OnyxTypes.ReportAction>> = { + report2: { + ...exampleReportAction, + person: report2PersonDiff, + }, + }; + + expect(reportActions).toEqual(expectedResult); + + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 5cb636e3a572..22c46203a53e 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import createOnyxMockUpdate from 'tests/utils/createOnyxMockUpdate'; import type {AppActionsMock} from '@libs/actions/__mocks__/App'; import * as AppImport from '@libs/actions/App'; import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; @@ -7,7 +8,6 @@ import type {OnyxUpdateManagerUtilsMock} from '@libs/actions/OnyxUpdateManager/u import type {ApplyUpdatesMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'; import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('@libs/actions/App'); @@ -18,34 +18,12 @@ const App = AppImport as AppActionsMock; const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; const OnyxUpdateManagerUtils = OnyxUpdateManagerUtilsImport as OnyxUpdateManagerUtilsMock; -const createMockUpdate = (lastUpdateID: number): OnyxUpdatesFromServer => ({ - type: 'https', - lastUpdateID, - previousUpdateID: lastUpdateID - 1, - request: { - command: 'TestCommand', - successData: [], - failureData: [], - finallyData: [], - optimisticData: [], - }, - response: { - lastUpdateID, - previousUpdateID: lastUpdateID - 1, - }, - updates: [ - { - eventType: 'test', - data: [], - }, - ], -}); -const mockUpdate3 = createMockUpdate(3); -const mockUpdate4 = createMockUpdate(4); -const mockUpdate5 = createMockUpdate(5); -const mockUpdate6 = createMockUpdate(6); -const mockUpdate7 = createMockUpdate(7); -const mockUpdate8 = createMockUpdate(8); +const mockUpdate3 = createOnyxMockUpdate(3); +const mockUpdate4 = createOnyxMockUpdate(4); +const mockUpdate5 = createOnyxMockUpdate(5); +const mockUpdate6 = createOnyxMockUpdate(6); +const mockUpdate7 = createOnyxMockUpdate(7); +const mockUpdate8 = createOnyxMockUpdate(8); describe('OnyxUpdateManager', () => { let lastUpdateIDAppliedToClient = 1; diff --git a/tests/utils/createOnyxMockUpdate.ts b/tests/utils/createOnyxMockUpdate.ts new file mode 100644 index 000000000000..f8e28e364cf7 --- /dev/null +++ b/tests/utils/createOnyxMockUpdate.ts @@ -0,0 +1,23 @@ +import type {OnyxUpdate} from 'react-native-onyx'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; + +const createOnyxMockUpdate = (lastUpdateID: number, successData: OnyxUpdate[] = []): OnyxUpdatesFromServer => ({ + type: 'https', + lastUpdateID, + previousUpdateID: lastUpdateID - 1, + request: { + command: 'TestCommand', + successData, + failureData: [], + finallyData: [], + optimisticData: [], + }, + response: { + jsonCode: 200, + lastUpdateID, + previousUpdateID: lastUpdateID - 1, + onyxData: successData, + }, +}); + +export default createOnyxMockUpdate; From d25bcdf9dcbd7bac51de93b8e5ee8375a5e32f42 Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Tue, 23 Apr 2024 15:56:12 +0200 Subject: [PATCH 302/580] Add template of SearchTableHeader --- src/languages/en.ts | 3 ++ src/languages/es.ts | 1 + src/libs/ReportUtils.ts | 5 ++++ src/pages/Search/SearchPage.tsx | 27 +++++++++++++++-- src/pages/Search/SearchTableHeader.tsx | 41 ++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/pages/Search/SearchTableHeader.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index fcb683472215..af77628415c3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -125,6 +125,7 @@ export default { buttonConfirm: 'Got it', name: 'Name', attachment: 'Attachment', + from: 'From', to: 'To', optional: 'Optional', new: 'New', @@ -324,6 +325,8 @@ export default { subtitleText3: 'button below.', }, businessName: 'Business name', + type: 'Type', + action: 'Action', }, location: { useCurrent: 'Use current location', diff --git a/src/languages/es.ts b/src/languages/es.ts index 57de4057e775..fcfd7ffa431a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -115,6 +115,7 @@ export default { buttonConfirm: 'Ok, entendido', name: 'Nombre', attachment: 'Archivo adjunto', + from: 'De', to: 'A', optional: 'Opcional', new: 'Nuevo', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a1e63bc5f48f..d3fc83e5553b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6147,6 +6147,10 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyID: s return isChatRoom(report) && !isThread(report); } +function shouldShowMerchantColumn(transactions: Transaction[]) { + return transactions.some((transaction) => isExpenseReport(allReports?.[transaction.reportID] ?? {})); +} + export { addDomainToShortMention, areAllRequestsBeingSmartScanned, @@ -6388,6 +6392,7 @@ export { updateOptimisticParentReportAction, updateReportPreview, temporary_getMoneyRequestOptions, + shouldShowMerchantColumn, }; export type { diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 3f69371841ce..cdf92344d945 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,9 +1,17 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; +import React, {useEffect, useState} from 'react'; +import {ValueOf} from 'type-fest'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import UserListItem from '@components/SelectionList/UserListItem'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import Navigation from '@libs/Navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; -import SearchResults from './SearchResults'; +import SearchTableHeader from './SearchTableHeader'; import useCustomBackHandler from './useCustomBackHandler'; type SearchPageProps = StackScreenProps<CentralPaneNavigatorParamList, typeof SCREENS.SEARCH.CENTRAL_PANE>; @@ -13,7 +21,20 @@ function SearchPage({route}: SearchPageProps) { return ( <ScreenWrapper testID={SearchPage.displayName}> - <SearchResults query={route.params.query} /> + <HeaderWithBackButton + title="All" + icon={Illustrations.MoneyReceipts} + shouldShowBackButton={false} + /> + <SelectionList + canSelectMultiple + headerContent={<SearchTableHeader />} + ListItem={UserListItem} + onSelectRow={() => {}} + sections={[]} + onCheckboxPress={() => {}} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + /> </ScreenWrapper> ); } diff --git a/src/pages/Search/SearchTableHeader.tsx b/src/pages/Search/SearchTableHeader.tsx new file mode 100644 index 000000000000..77fd5300d6aa --- /dev/null +++ b/src/pages/Search/SearchTableHeader.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as ReportUtils from '@libs/ReportUtils'; +import type {Transaction} from '@src/types/onyx'; + +type SearchTableHeaderProps = { + data?: Transaction[]; + onSelectAll?: () => void; +}; + +function SearchTableHeader({data, onSelectAll}: SearchTableHeaderProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + + // const showMerchantColumn = ReportUtils.shouldShowMerchantColumn(data); + const showMerchantColumn = isSmallScreenWidth && true; + + return ( + <View style={[styles.flexRow, styles.justifyContentBetween, styles.pl3, styles.pr9]}> + <Text style={styles.searchInputStyle}>{translate('common.receipt')}</Text> + <Text style={[styles.searchInputStyle]}>{translate('common.date')}</Text> + {showMerchantColumn && <Text style={[styles.searchInputStyle]}>{translate('common.merchant')}</Text>} + <Text style={[styles.searchInputStyle]}>{translate('common.description')}</Text> + <Text style={[styles.searchInputStyle, styles.textAlignCenter]}>{translate('common.from')}</Text> + <Text style={[styles.searchInputStyle, styles.textAlignCenter]}>{translate('common.to')}</Text> + <Text style={[styles.searchInputStyle, styles.textAlignCenter]}>{translate('common.category')}</Text> + <Text style={[styles.searchInputStyle, styles.textAlignCenter, styles.textAlignRight]}>{translate('common.total')}</Text> + <Text style={[styles.searchInputStyle, styles.textAlignCenter, styles.textAlignRight]}>{translate('common.type')}</Text> + <Text style={[styles.searchInputStyle, styles.textAlignCenter, styles.textAlignRight]}>{translate('common.action')}</Text> + </View> + ); +} + +SearchTableHeader.displayName = 'SearchTableHeader'; + +export default SearchTableHeader; From 6b41cdec96ff1cfb8ce8a59791f8000027def43c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Tue, 23 Apr 2024 17:10:22 +0200 Subject: [PATCH 303/580] restrict actions for read-only chats --- .../home/report/ContextMenu/ContextMenuActions.tsx | 12 ++++++++++++ src/pages/home/report/ReportActionItem.tsx | 2 ++ 2 files changed, 14 insertions(+) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index eb1e60461005..5638e08d3b34 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -513,5 +513,17 @@ const ContextMenuActions: ContextMenuAction[] = [ }, ]; +const restrictedReadOnlyActions = [ + 'common.download', + 'reportActionContextMenu.replyInThread', + 'reportActionContextMenu.editAction', + 'reportActionContextMenu.joinThread', + 'reportActionContextMenu.deleteAction', +]; + +// @ts-expect-error Not all actions have textTranslateKey +const RestrictedReadOnlyContextMenuActions: ContextMenuAction[] = ContextMenuActions.filter((action) => restrictedReadOnlyActions.includes(action.textTranslateKey)); + +export {RestrictedReadOnlyContextMenuActions}; export default ContextMenuActions; export type {ContextMenuActionPayload, ContextMenuAction}; diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index c07b693001e0..b2dc4550f8fd 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -66,6 +66,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {OriginalMessageActionableMentionWhisper, OriginalMessageActionableTrackedExpenseWhisper, OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; +import {RestrictedReadOnlyContextMenuActions} from './ContextMenu/ContextMenuActions'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import {hideContextMenu} from './ContextMenu/ReportActionContextMenu'; @@ -914,6 +915,7 @@ function ReportActionItem({ originalReportID={originalReportID ?? ''} isArchivedRoom={ReportUtils.isArchivedRoom(report)} displayAsGroup={displayAsGroup} + disabledActions={ReportUtils.isReadOnly(report) ? RestrictedReadOnlyContextMenuActions : []} isVisible={hovered && draftMessage === undefined && !hasErrors} draftMessage={draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} From afdeb9d7ca5131dee5bc495f5fba8198794f8c5e Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:27:08 -0400 Subject: [PATCH 304/580] Delete docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md https://github.com/Expensify/Expensify/issues/381301 this resource was moved to a new category -- deleting --- .../Global-Reimbursements.md | 106 ------------------ 1 file changed, 106 deletions(-) delete mode 100644 docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md diff --git a/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md b/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md deleted file mode 100644 index 2ff74760b376..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/Global-Reimbursements.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: International Reimbursements -description: International Reimbursements ---- -# Overview - -If your company’s business bank account is in the US, Canada, the UK, Europe, or Australia, you now have the option to send direct reimbursements to nearly any country across the globe! -The process to enable global reimbursements is dependent on the currency of your reimbursement bank account, so be sure to review the corresponding instructions below. - -# How to request international reimbursements - -## The reimbursement account is in USD - -If your reimbursement bank account is in USD, the first step is connecting the bank account to Expensify. -The individual who plans on sending reimbursements internationally should head to **Settings > Account > Payments > Add Verified Bank Account**. From there, you will provide company details, input personal information, and upload a copy of your ID. - -Once the USD bank account is verified (or if you already had a USD business bank account connected), click the support icon in your Expensify account to inform your Setup Specialist, Account Manager, or Concierge that you’d like to enable international reimbursements. From there, Expensify will ask you to confirm the currencies of the reimbursement and employee bank accounts. - -Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. - -## The reimbursement account is in AUD, CAD, GBP, EUR - -To request international reimbursements, contact Expensify Support to make that request. - -You can do this by clicking on the support icon and informing your Setup Specialist, Account Manager, or Concierge that you’d like to set up global reimbursements on your account. -From there, Expensify will ask you to confirm both the currencies of the reimbursement and employee bank accounts. - -Our team will assess your account, and if you meet the criteria, international reimbursements will be enabled. - -# How to verify the bank account for sending international payments - -Once international payments are enabled on your Expensify account, the next step is verifying the bank account to send the reimbursements. - -## The reimbursement account is in USD - -First, confirm the workspace settings are set up correctly by doing the following: -1. Head to **Settings > Workspaces > Group > _[Workspace Name]_ > Reports** and check that the workspace currency is USD -2. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the reimbursement method to direct -3. Under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements**, set the USD bank account to the default account - -Once that’s all set, head to **Settings > Account > Payments**, and click **Enable Global Reimbursement** on the bank account (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account). - -From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. - -## The reimbursement account is in AUD, CAD, GBP, EUR - -First, confirm the workspace currency corresponds with the currency of the reimbursement bank account. You can do this under **Settings > Workspaces > Group > _[Workspace Name]_ > Reports**. It should be AUD, CAD, GBP, or EUR. - -Next, add the bank account to Expensify: -1. Head to **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements** and set the reimbursement method to direct (this button may not show for up to 60 minutes after the Expensify team confirms international reimbursements are available on your account) -2. Click **Add Business Bank Account** -3. If the incorrect country shows as the default, click **Switch Country** to select the correct country -4. Enter the bank account details -5. Click **Save & Continue** - -From there, you’ll fill out a form via DocuSign. Once the form is complete, it is automatically sent to our Compliance Team for review. Our Support Team will contact you with more details if additional information is required. - -# How to start reimbursing internationally - -After the bank account is verified for international payments, set the correct bank account as the reimbursement account. - -You can do this under **Settings > Workspaces > Group > _[Workspace Name]_ > Reimbursements** by selecting the reimbursement account as the default account. - -Finally, have your employees add their deposit-only bank accounts. They can do this by logging into their Expensify accounts, heading to **Settings > Account > Payments**, and clicking **Add Deposit-Only Bank Account**. - -# Deep Dive - -## Documents requested - -Our Compliance Team may ask for additional information depending on who initiates the verification or what information you provide on the DocuSign form. - -Examples of additional requested information: -- The reimburser’s proof of address and ID -- Company directors’ proofs of address and IDs -- An authorization letter -- An independently certified documentation such as shareholder agreement from a lawyer, notary, or public accountant if an individual owns more than 25% of the company - -{% include faq-begin.md %} - -## How many people can send reimbursements internationally? - -Once your company is authorized to send global payments, the individual who verified the bank account can share it with additional admins on the workspace. That way, multiple workspace members can send international reimbursements. - -## How long does it take to verify an account for international payments? - -It varies! The verification process can take a few business days to several weeks. It depends on whether or not the information in the DocuSign form is correct if our Compliance Team requires any additional information, and how responsive the employee verifying the company’s details is to our requests. - -## If I already have a USD bank account connected to Expensify, do I need to go through the verification process again to enable international payments? - -If you’ve already connected a US business bank account, you can request to enable global reimbursements by contacting Expensify Support immediately. However, additional steps are required to verify the bank account for international payments. - -## My employee says they don’t have the option to add their non-USD bank account as a deposit account – what should they do? - -Have the employee double-check that their default workspace is set as the workspace that's connected to the bank you're using to send international payments. - -An employee can confirm their default workspace is under **Settings > Workspaces > Group**. The default workspace has a green checkmark next to it. They can change their default workspace by clicking **Default Workspace** on the correct workspace. - -## Who is the “Authorized User” on the International Reimbursement DocuSign form? - -This is the person who will process international reimbursements. The authorized user should be the same person who manages the bank account connection in Expensify. - -## Who should I enter as the “User” on the International Reimbursement form? - -You can leave this form section blank since the “User” is Expensify. - -{% include faq-end.md %} From 5893beba8bfcfabbe935b1d3c36bf10c6b0ab3a0 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:33:31 -0400 Subject: [PATCH 305/580] Delete docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md https://github.com/Expensify/Expensify/issues/381301 this content was moved to a different category -- deleting --- .../Approval-Workflows.md | 109 ------------------ 1 file changed, 109 deletions(-) delete mode 100644 docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md diff --git a/docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md b/docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md deleted file mode 100644 index 4c64ab1cefe4..000000000000 --- a/docs/articles/expensify-classic/copilots-and-delegates/Approval-Workflows.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Managing employees and reports > Approval workflows -description: Set up the workflow that your employees reports should flow through. ---- -<!-- The lines above are required by Jekyll to process the .md file --> - -# About -## Overview - - -This document explains how to manage employee expense reports and approval workflows in Expensify. - - -### Approval workflow modes - - -#### Submit and close -- This is a workflow where no approval occurs in Expensify. -- *What happens after submission?* The report state becomes Closed and is available to view by the member set in Submit reports to and any Workspace Admins. -- *Who should use this workflow?* This mode should be used where you don't require approvals in Expensify. - - -#### Submit and approve -- *Submit and approve* is a workflow where all reports are submitted to a single member for approval. New policies have Submit and Approve enabled by default. -- *What happens after submission?* The report state becomes Processing and it will be sent to the member indicated in Submit reports to for approval. When the member approves the report, the state will become Approved. -- *Who should use this workflow?* This mode should be used where the same person is responsible for approving all reports for your organization. If submitters have different approvers or multiple levels of approval are required, then you will need to use Advance Approval. - - -#### Advanced Approval -- This approval mode is used to handle more complex workflows, including: - - *Multiple levels of approval.* This is for companies that require more than one person to approve a report before it can be reimbursed. The most common scenario is when an employee needs to submit to their manager, and their manager needs to approve and forward that report to their finance department for final approval. - - *Varying approval workflows.* For example, if a company has Team A submitting reports to Manager A, and Team B to Manager B, use Advanced Approval. Group Workspace Admins can also set amount thresholds in the case that a report needs to go to a different approver based on the amount. -- *What happens after submission?* After the report is submitted, it will follow the set approval chain. The report state will be Processing until it is Final Approved. We have provided examples of how to set this up below. -- *Who should use this workflow?* Organizations with complex workflows or 2+ levels of approval. This could be based on manager approvals or where reports over a certain size require additional approvals. -- *For further automation:* use Concierge auto-approval for reports. You can set specific rules and guidelines in your Group Workspace for your team's expenses; if all expenses are below the Manual Approval Threshold and adhere to all the rules, then we will automatically approve these reports on behalf of the approver right after they are submitted. - - -### How to set an approval workflow - -- Step-by-step instructions on how to set this up at the Workspace level [here](link-to-instructions). - -# Deep Dive - -### Setting multiple levels of approval -- 'Submits to' is different than 'Approves to'. - - *Submits to* - is the person you are sending your reports to for 1st level approval - - *Approves to* - is the person you are sending the reports you've approved for higher-level approval -- In the example below, a report needs to be approved by multiple managers: *Submitter > Manager > Director > Finance/Accountant* - - *Submitter (aka. Employee):* This is the person listed under the member column of the People page. - - *First Approver (Manager):* This is the person listed under the Submits to column of the People Page. - - *Second Approver (Director):* This is the person listed as 'Approves to' in the Settings of the First Approver. - - *Final Approver (Finance/Accountant):* This is the person listed as the 'Approves to' in the Settings of the Second Approver. -- This is what this setup looks like in the Workspace Members table. - - Bryan submits his reports to Jim for 1st level approval. -![Screenshot showing the People section of the workspace]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_1.png){:width="100%"} - - - All of the reports Jim approves are submitted to Kevin. Kevin is the 'approves to' in Jim's Settings. -![Screenshot of Policy Member Editor]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_2.png){:width="100%"} - - - All of the reports Kevin approves are submitted to Lucy. Lucy is the 'approves to' in Kevin's Settings. -![Screenshot of Policy Member Editor Approves to]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_3.png){:width="100%"} - - - - Lucy is the final approver, so she doesn't submit her reports to anyone for review. -![Screenshot of Policy Member Editor Final Approver]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_4.png){:width="100%"} - - -- The final outcome: The member in the Submits To line is different than the person noted as the Approves To. -### Adding additional approver levels -- You can also set a specific approver for Reports Totals in Settings. -![Screenshot of Policy Member Editor Approves to]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_5.png){:width="100%"} - -- An example: The submitter's manager can approve any report up to a certain limit, let's say $500, and forward it to accounting. However, if a report is over that $500 limit, it has to be also approved by the department head before being forwarded to accounting. -- To configure, click on Edit Settings next to the approving manager's email address and set the "If Report Total is Over" and "Then Approves to" fields. -![Screenshot of Workspace Member Settings]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_6.png){:width="100%"} -![Screenshot of Policy Member Editor Configure]({{site.url}}/assets/images/ManagingEmployeesAndReports_ApprovalWorkflows_7.png){:width="100%"} - - -### Setting category approvals -- If your expense reports should be reviewed by an additional approver based on specific categories or tags selected on the expenses within the report, set up category approvers and tag approvers. -- Category approvers can be set in the Category settings for each Workspace -- Tag approvers can be set in the Tag settings for each Workspace - - -#### Category approver -- A category approver is a member who is added to the approval workflow for any reports in your Expensify Workspace that contain expenses with a specific category. -- For example: Your HR director Jim may need to approve any relocation expenses submitted by employees. Set Jim up as the category approver for your Relocation category, then any reports containing Relocation expenses will first be routed to Jim before continuing through the approval workflow. -- Adding category approvers - - To add a category approver in your Workspace: - - Navigate to *Settings > Policies > Group > [Workspace Name] > Categories* - - Click *"Edit Settings"* next to the category that requires the additional approver - - Select an approver and click *"Save"* - - -#### Tag approver -- A tag approver is a member who is added to the approval workflow for any reports in your Expensify Workspace that contain expenses with a specific tag. -- For example: If employees must tag project-based expenses with the corresponding project tag. Pam, the project manager is set as the project approver for that project, then any reports containing expenses with that project tag will first be routed to Pam for approval before continuing through the approval workflow. -- Please note: Tag approvers are only supported for a single level of tags, not for multi-level tags. The order in which the report is sent to tag approvers relies on the date of the expense. -- Adding tag approvers - - To add a tag approver in your Workspace: - - Navigate to *Settings > Policies > Group > [Workspace Name] > Tags* - - Click in the "Approver" column next to the tag that requires an additional approver - - -Category and Tag approvers are inserted at the beginning of the approval workflow already set on the People page. This means the workflow will look something like: * *Submitter > Category Approver(s) > Tag Approver(s) > Submits To > Previous approver's Approves To.* - - -### Workflow enforcement -- If you want to ensure your employees cannot override the workflow you set - enable workflow enforcement. As a Workspace Admin, you can choose to enforce your approval workflow by going to Settings > Workspaces > Group > [Workspace Name] > People > Approval Mode. When enabled (which is the default setting for a new workspace), submitters and approvers must adhere to the set approval workflow (recommended). This setting does not apply to Workspace Admins, who are free to submit outside of this workflow From d0e3b689baa07c679e77077fb97f5b2c3511e643 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 17:36:12 +0200 Subject: [PATCH 306/580] add more tests and comments --- tests/actions/OnyxUpdateManagerTest.ts | 131 +++++++++++++++++++++---- 1 file changed, 113 insertions(+), 18 deletions(-) diff --git a/tests/actions/OnyxUpdateManagerTest.ts b/tests/actions/OnyxUpdateManagerTest.ts index e51285879e3b..f8a2f8fcb98f 100644 --- a/tests/actions/OnyxUpdateManagerTest.ts +++ b/tests/actions/OnyxUpdateManagerTest.ts @@ -6,6 +6,8 @@ import * as AppImport from '@libs/actions/App'; import applyOnyxUpdatesReliably from '@libs/actions/applyOnyxUpdatesReliably'; import * as OnyxUpdateManagerExports from '@libs/actions/OnyxUpdateManager'; import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; +import * as OnyxUpdateManagerUtilsImport from '@libs/actions/OnyxUpdateManager/utils'; +import type {OnyxUpdateManagerUtilsMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__'; import type {ApplyUpdatesMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'; import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import CONST from '@src/CONST'; @@ -13,7 +15,6 @@ import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import createOnyxMockUpdate from '../utils/createOnyxMockUpdate'; -import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('@libs/actions/App'); @@ -28,12 +29,13 @@ jest.mock('@libs/actions/OnyxUpdateManager/utils/applyUpdates', () => { const App = AppImport as AppActionsMock; const ApplyUpdates = ApplyUpdatesImport as ApplyUpdatesMock; +const OnyxUpdateManagerUtils = OnyxUpdateManagerUtilsImport as OnyxUpdateManagerUtilsMock; const TEST_USER_ACCOUNT_ID = 1; const REPORT_ID = 'testReport1'; const ONYX_KEY = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}` as const; -const exampleReportAction: Partial<OnyxTypes.ReportAction> = { +const exampleReportAction = { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, actorAccountID: TEST_USER_ACCOUNT_ID, automatic: false, @@ -41,10 +43,17 @@ const exampleReportAction: Partial<OnyxTypes.ReportAction> = { message: [{type: 'COMMENT', html: 'Testing a comment', text: 'Testing a comment', translationKey: ''}], person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], shouldShow: true, -}; +} satisfies Partial<OnyxTypes.ReportAction>; -const initialData = {report1: exampleReportAction, report2: exampleReportAction, report3: exampleReportAction} as OnyxTypes.ReportActions; +const initialData = {report1: exampleReportAction, report2: exampleReportAction, report3: exampleReportAction} as unknown as OnyxTypes.ReportActions; +const mockUpdate1 = createOnyxMockUpdate(1, [ + { + onyxMethod: OnyxUtils.METHOD.SET, + key: ONYX_KEY, + value: initialData, + }, +]); const mockUpdate2 = createOnyxMockUpdate(2, [ { onyxMethod: OnyxUtils.METHOD.MERGE, @@ -55,21 +64,22 @@ const mockUpdate2 = createOnyxMockUpdate(2, [ }, ]); -const report2PersonDiff = [ - {type: 'TEXT', style: 'light', text: 'Other Test User'}, - {type: 'TEXT', style: 'light', text: 'Other Test User 2'}, -]; +const report2PersonDiff = { + person: [ + {type: 'TEXT', style: 'light', text: 'Other Test User'}, + {type: 'TEXT', style: 'light', text: 'Other Test User 2'}, + ], +} satisfies Partial<OnyxTypes.ReportAction>; +const report3AvatarDiff: Partial<OnyxTypes.ReportAction> = { + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_5.png', +}; const mockUpdate3 = createOnyxMockUpdate(3, [ { onyxMethod: OnyxUtils.METHOD.MERGE, key: ONYX_KEY, value: { - report2: { - person: report2PersonDiff, - }, - report3: { - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_5.png', - }, + report2: report2PersonDiff, + report3: report3AvatarDiff, }, }, ]); @@ -83,6 +93,24 @@ const mockUpdate4 = createOnyxMockUpdate(4, [ }, ]); +const report2AvatarDiff = { + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_6.png', +} satisfies Partial<OnyxTypes.ReportAction>; +const report4 = { + ...exampleReportAction, + automatic: true, +} satisfies Partial<OnyxTypes.ReportAction>; +const mockUpdate5 = createOnyxMockUpdate(5, [ + { + onyxMethod: OnyxUtils.METHOD.MERGE, + key: ONYX_KEY, + value: { + report2: report2AvatarDiff, + report4, + }, + }, +]); + OnyxUpdateManager(); describe('actions/OnyxUpdateManager', () => { @@ -96,8 +124,6 @@ describe('actions/OnyxUpdateManager', () => { }); beforeEach(async () => { - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - global.fetch = TestHelper.getGlobalFetchMock(); jest.clearAllMocks(); await Onyx.clear(); await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); @@ -109,7 +135,10 @@ describe('actions/OnyxUpdateManager', () => { }); it('should trigger Onyx update gap handling', async () => { - App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate3]; + // Since we don't want to trigger actual GetMissingOnyxUpdates calls to the server/backend, + // we have to mock the results of these calls. By setting the missingOnyxUpdatesToBeApplied + // property on the mock, we can simulate the results of the GetMissingOnyxUpdates calls. + App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate2, mockUpdate3]; applyOnyxUpdatesReliably(mockUpdate2); applyOnyxUpdatesReliably(mockUpdate4); @@ -119,13 +148,79 @@ describe('actions/OnyxUpdateManager', () => { const expectedResult: Record<string, Partial<OnyxTypes.ReportAction>> = { report2: { ...exampleReportAction, - person: report2PersonDiff, + ...report2PersonDiff, }, }; expect(reportActions).toEqual(expectedResult); + // GetMissingOnyxUpdates should have been called for the gap between update 2 and 4. + // Since we queued update 4 before update 3, there's a gap to resolve, before we apply the deferred updates. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 2, 3); + + // After the missing updates have been applied, the applicable updates after + // all locally applied updates should be applied. (4) expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {4: mockUpdate4}); }); }); + + it('should trigger 2 GetMissingOnyxUpdates calls, because the deferred updates have gaps', async () => { + // Since we don't want to trigger actual GetMissingOnyxUpdates calls to the server/backend, + // we have to mock the results of these calls. By setting the missingOnyxUpdatesToBeApplied + // property on the mock, we can simulate the results of the GetMissingOnyxUpdates calls. + App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate1, mockUpdate2]; + + applyOnyxUpdatesReliably(mockUpdate3); + applyOnyxUpdatesReliably(mockUpdate5); + + let finishFirstCall: () => void; + const firstGetMissingOnyxUpdatesCallFinished = new Promise<void>((resolve) => { + finishFirstCall = resolve; + }); + + OnyxUpdateManagerUtils.mockValues.onValidateAndApplyDeferredUpdates = () => { + finishFirstCall(); + return Promise.resolve(); + }; + + return firstGetMissingOnyxUpdatesCallFinished + .then(() => { + // After the first GetMissingOnyxUpdates call has been resolved, + // we have to set the mocked results of for the second call. + App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate3, mockUpdate4]; + }) + .then(() => OnyxUpdateManagerExports.queryPromise) + .then(() => { + const expectedResult: Record<string, Partial<OnyxTypes.ReportAction>> = { + report2: { + ...exampleReportAction, + ...report2PersonDiff, + ...report2AvatarDiff, + }, + report4, + }; + + expect(reportActions).toEqual(expectedResult); + + // GetMissingOnyxUpdates should have been called twice, once for the gap between update 1 and 3, + // and once for the gap between update 3 and 5. + // We always fetch missing updates from the lastUpdateIDAppliedToClient + // to previousUpdateID of the first deferred update. First 1-2, second 3-4 + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); + + // Since we have two GetMissingOnyxUpdates calls, there will be two sets of applicable updates. + // The first applicable update will be 3, after missing updates 1-2 have been applied. + // The second applicable update will be 5, after missing updates 3-4 have been applied. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5}); + }); + }); }); From 52ad15a5a958ebc55d7755a1641da12134a2929e Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 17:41:37 +0200 Subject: [PATCH 307/580] remove waitForBatchedChanges --- tests/unit/OnyxUpdateManagerTest.ts | 266 +++++++++++++--------------- 1 file changed, 126 insertions(+), 140 deletions(-) diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index 22c46203a53e..e2bd0daec081 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -1,5 +1,4 @@ import Onyx from 'react-native-onyx'; -import createOnyxMockUpdate from 'tests/utils/createOnyxMockUpdate'; import type {AppActionsMock} from '@libs/actions/__mocks__/App'; import * as AppImport from '@libs/actions/App'; import * as OnyxUpdateManager from '@libs/actions/OnyxUpdateManager'; @@ -8,7 +7,7 @@ import type {OnyxUpdateManagerUtilsMock} from '@libs/actions/OnyxUpdateManager/u import type {ApplyUpdatesMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'; import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import createOnyxMockUpdate from '../utils/createOnyxMockUpdate'; jest.mock('@libs/actions/App'); jest.mock('@libs/actions/OnyxUpdateManager/utils'); @@ -38,7 +37,6 @@ describe('OnyxUpdateManager', () => { jest.clearAllMocks(); await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); OnyxUpdateManager.resetDeferralLogicVariables(); - return waitForBatchedUpdates(); }); it('should fetch missing Onyx updates once, defer updates and apply after missing updates', () => { @@ -46,27 +44,25 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate4); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); - return waitForBatchedUpdates() - .then(() => OnyxUpdateManager.queryPromise) - ?.then(() => { - // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - expect(lastUpdateIDAppliedToClient).toBe(5); - - // There are no gaps in the deferred updates, therefore only one call to getMissingOnyxUpdates should be triggered - expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); - - // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. - // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. - // The intended assertion would look like this: - // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); - - // There should be only one call to applyUpdates. The call should contain all the deferred update, - // since the locally applied updates have changed in the meantime. - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); - }); + return OnyxUpdateManager.queryPromise.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(5); + + // There are no gaps in the deferred updates, therefore only one call to getMissingOnyxUpdates should be triggered + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + + // There should be only one call to applyUpdates. The call should contain all the deferred update, + // since the locally applied updates have changed in the meantime. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({3: mockUpdate3, 4: mockUpdate4, 5: mockUpdate5}); + }); }); it('should only apply deferred updates that are newer than the last locally applied update (pending updates)', async () => { @@ -82,31 +78,29 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManagerUtils.mockValues.onValidateAndApplyDeferredUpdates = undefined; }; - return waitForBatchedUpdates() - .then(() => OnyxUpdateManager.queryPromise) - ?.then(() => { - // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - expect(lastUpdateIDAppliedToClient).toBe(6); + return OnyxUpdateManager.queryPromise.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(6); - // There are no gaps in the deferred updates, therefore only one call to getMissingOnyxUpdates should be triggered - expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + // There are no gaps in the deferred updates, therefore only one call to getMissingOnyxUpdates should be triggered + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); - // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. - // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. - // The intended assertion would look like this: - // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); - // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server in the first and only call to getMissingOnyxUpdates - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); + // Missing updates from 1 (last applied to client) to 3 (last "previousUpdateID" from first deferred update) should have been fetched from the server in the first and only call to getMissingOnyxUpdates + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 3); - // There should be only one call to applyUpdates. The call should only contain the last deferred update, - // since the locally applied updates have changed in the meantime. - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); + // There should be only one call to applyUpdates. The call should only contain the last deferred update, + // since the locally applied updates have changed in the meantime. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); - }); + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledWith({6: mockUpdate6}); + }); }); it('should re-fetch missing updates if the deferred updates have a gap', async () => { @@ -114,36 +108,34 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); - return waitForBatchedUpdates() - .then(() => OnyxUpdateManager.queryPromise) - ?.then(() => { - // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - expect(lastUpdateIDAppliedToClient).toBe(6); + return OnyxUpdateManager.queryPromise.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(6); - // Even though there is a gap in the deferred updates, we only want to fetch missing updates once per batch. - expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + // Even though there is a gap in the deferred updates, we only want to fetch missing updates once per batch. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); - // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. - // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. - // The intended assertion would look like this: - // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); - // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. - // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); + // There should be multiple calls getMissingOnyxUpdates and applyUpdates, since we detect a gap in the deferred updates. + // The first call to getMissingOnyxUpdates should fetch updates from 1 (last applied to client) to 2 (last "previousUpdateID" from first deferred update) from the server. + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); - // After the initial missing updates have been applied, the applicable updates (3) should be applied. - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + // After the initial missing updates have been applied, the applicable updates (3) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); - // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap in the deferred updates. 3-4 - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); + // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap in the deferred updates. 3-4 + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); - // After the gap in the deferred updates has been resolved, the remaining deferred updates (5, 6) should be applied. - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5, 6: mockUpdate6}); - }); + // After the gap in the deferred updates has been resolved, the remaining deferred updates (5, 6) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5, 6: mockUpdate6}); + }); }); it('should re-fetch missing deferred updates only once per batch', async () => { @@ -152,34 +144,32 @@ describe('OnyxUpdateManager', () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate8); - return waitForBatchedUpdates() - .then(() => OnyxUpdateManager.queryPromise) - ?.then(() => { - // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - expect(lastUpdateIDAppliedToClient).toBe(8); - - // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. - expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); - // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. - // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. - // The intended assertion would look like this: - // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); - - // After the initial missing updates have been applied, the applicable updates (3-4) should be applied. - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3, 4: mockUpdate4}); - - // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap (4-7) in the deferred updates. - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 4, 7); - - // After the gap in the deferred updates has been resolved, the remaining deferred updates (8) should be applied. - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {8: mockUpdate8}); - }); + return OnyxUpdateManager.queryPromise.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(8); + + // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); + + // After the initial missing updates have been applied, the applicable updates (3-4) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3, 4: mockUpdate4}); + + // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap (4-7) in the deferred updates. + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 4, 7); + + // After the gap in the deferred updates has been resolved, the remaining deferred updates (8) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {8: mockUpdate8}); + }); }); - it('should not re-fetch missing updates if the locally applied update has been updated', async () => { + it('should not re-fetch missing updates if the lastUpdateIDFromClient has been updated', async () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate5); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate6); @@ -194,35 +184,33 @@ describe('OnyxUpdateManager', () => { ApplyUpdates.mockValues.onApplyUpdates = undefined; }; - return waitForBatchedUpdates() - .then(() => OnyxUpdateManager.queryPromise) - ?.then(() => { - // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - expect(lastUpdateIDAppliedToClient).toBe(7); + return OnyxUpdateManager.queryPromise.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(7); - // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. - expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); + // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); - // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. - // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. - // The intended assertion would look like this: - // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(1); - // Since there is a gap in the deferred updates, we need to run applyUpdates twice. - // Once for the applicable updates (before the gap) and then for the remaining deferred updates. - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + // Since there is a gap in the deferred updates, we need to run applyUpdates twice. + // Once for the applicable updates (before the gap) and then for the remaining deferred updates. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); - // After the initial missing updates have been applied, the applicable updates (3) should be applied. - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + // After the initial missing updates have been applied, the applicable updates (3) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); - // Since the lastUpdateIDAppliedToClient has changed to 5 in the meantime, we only need to apply the remaining deferred updates (6-7). - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {6: mockUpdate6, 7: mockUpdate7}); - }); + // Since the lastUpdateIDAppliedToClient has changed to 5 in the meantime, we only need to apply the remaining deferred updates (6-7). + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {6: mockUpdate6, 7: mockUpdate7}); + }); }); - it('should re-fetch missing updates if the locally applied update has been updated, but there are still gaps after the locally applied update', async () => { + it('should re-fetch missing updates if the lastUpdateIDFromClient has increased, but there are still gaps after the locally applied update', async () => { OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate3); OnyxUpdateManager.handleOnyxUpdateGap(mockUpdate7); @@ -235,35 +223,33 @@ describe('OnyxUpdateManager', () => { ApplyUpdates.mockValues.onApplyUpdates = undefined; }; - return waitForBatchedUpdates() - .then(() => OnyxUpdateManager.queryPromise) - ?.then(() => { - // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. - expect(lastUpdateIDAppliedToClient).toBe(7); + return OnyxUpdateManager.queryPromise.then(() => { + // After all missing and deferred updates have been applied, the lastUpdateIDAppliedToClient should be 6. + expect(lastUpdateIDAppliedToClient).toBe(7); - // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. - expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + // Even though there are multiple gaps in the deferred updates, we only want to fetch missing updates once per batch. + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); - // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. - // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. - // The intended assertion would look like this: - // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); + // validateAndApplyDeferredUpdates should be called twice, once for the initial deferred updates and once for the remaining deferred updates with gaps. + // Unfortunately, we cannot easily count the calls of this function with Jest, since it recursively calls itself. + // The intended assertion would look like this: + // expect(OnyxUpdateManagerUtils.validateAndApplyDeferredUpdates).toHaveBeenCalledTimes(2); - // Since there is a gap in the deferred updates, we need to run applyUpdates twice. - // Once for the applicable updates (before the gap) and then for the remaining deferred updates. - expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); + // Since there is a gap in the deferred updates, we need to run applyUpdates twice. + // Once for the applicable updates (before the gap) and then for the remaining deferred updates. + expect(ApplyUpdates.applyUpdates).toHaveBeenCalledTimes(2); - // After the initial missing updates have been applied, the applicable updates (3) should be applied. - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); + // After the initial missing updates have been applied, the applicable updates (3) should be applied. + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(1, {3: mockUpdate3}); - // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap in the deferred updates, - // that are later than the locally applied update (4-6). (including the last locally applied update) - expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 4, 6); + // The second call to getMissingOnyxUpdates should fetch the missing updates from the gap in the deferred updates, + // that are later than the locally applied update (4-6). (including the last locally applied update) + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 4, 6); - // Since the lastUpdateIDAppliedToClient has changed to 4 in the meantime and we're fetching updates 5-6 we only need to apply the remaining deferred updates (7). - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {7: mockUpdate7}); - }); + // Since the lastUpdateIDAppliedToClient has changed to 4 in the meantime and we're fetching updates 5-6 we only need to apply the remaining deferred updates (7). + // eslint-disable-next-line @typescript-eslint/naming-convention + expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {7: mockUpdate7}); + }); }); }); From 0269d586a48ea36fe4f8e4b67cbe67048c340e8c Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Tue, 23 Apr 2024 22:52:55 +0700 Subject: [PATCH 308/580] move action param to first --- src/ROUTES.ts | 2 +- src/components/MoneyRequestConfirmationList.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 681b72c2da6a..bd1fec98ae80 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -384,7 +384,7 @@ const ROUTES = { }, MONEY_REQUEST_STEP_SPLIT_PAYER: { route: ':action/:iouType/confirmation/:transactionID/:reportID/payer', - getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '', action: ValueOf<typeof CONST.IOU.ACTION> = 'create') => + getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`${action}/${iouType}/confirmation/${transactionID}/${reportID}/payer`, backTo), }, MONEY_REQUEST_STEP_SCAN: { diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 496621993885..740933dd166b 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -695,7 +695,7 @@ function MoneyRequestConfirmationList({ title={payeePersonalDetails.displayName ?? ReportUtils.getDisplayNameForParticipant(payeePersonalDetails.accountID)} icon={payeeIcons} onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); }} shouldShowRightIcon={!isPolicyExpenseChat && !isReadOnly} titleWithTooltips={payeePersonalDetails?.isOptimisticPersonalDetail ? undefined : payeeTooltipDetails} From a7aac11d1e50781c4c359be55b6e867130fd9e7a Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 18:17:45 +0200 Subject: [PATCH 309/580] rename and move helper function --- .../OnyxUpdateManager/utils/__mocks__/applyUpdates.ts | 4 ++-- .../actions/OnyxUpdateManager/utils/__mocks__/index.ts | 4 ++-- .../actions/OnyxUpdateManager/utils/deferredUpdates.ts | 4 ++-- src/libs/actions/__mocks__/App.ts | 4 ++-- .../{createProxyForValue.ts => createProxyForObject.ts} | 9 +++++++-- 5 files changed, 15 insertions(+), 10 deletions(-) rename src/utils/{createProxyForValue.ts => createProxyForObject.ts} (56%) diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts index d02a034c29cc..5cd66df6b0b0 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates.ts @@ -1,7 +1,7 @@ import Onyx from 'react-native-onyx'; import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import createProxyForValue from '@src/utils/createProxyForValue'; +import createProxyForObject from '@src/utils/createProxyForObject'; let lastUpdateIDAppliedToClient = 0; Onyx.connect({ @@ -21,7 +21,7 @@ type ApplyUpdatesMock = { const mockValues: ApplyUpdatesMockValues = { onApplyUpdates: undefined, }; -const mockValuesProxy = createProxyForValue(mockValues); +const mockValuesProxy = createProxyForObject(mockValues); const applyUpdates = jest.fn((updates: DeferredUpdatesDictionary) => { const lastUpdateIdFromUpdates = Math.max(...Object.keys(updates).map(Number)); diff --git a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts index e548a9e22b1a..b4d97a4399db 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/__mocks__/index.ts @@ -1,5 +1,5 @@ import type {DeferredUpdatesDictionary, DetectGapAndSplitResult} from '@libs/actions/OnyxUpdateManager/types'; -import createProxyForValue from '@src/utils/createProxyForValue'; +import createProxyForObject from '@src/utils/createProxyForObject'; import type * as OnyxUpdateManagerUtilsImport from '..'; import {applyUpdates} from './applyUpdates'; @@ -18,7 +18,7 @@ type OnyxUpdateManagerUtilsMock = typeof UtilsImplementation & { const mockValues: OnyxUpdateManagerUtilsMockValues = { onValidateAndApplyDeferredUpdates: undefined, }; -const mockValuesProxy = createProxyForValue(mockValues); +const mockValuesProxy = createProxyForObject(mockValues); const detectGapsAndSplit = jest.fn(UtilsImplementation.detectGapsAndSplit); diff --git a/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts b/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts index 1bf251fbd3df..838c27821aae 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/deferredUpdates.ts @@ -1,8 +1,8 @@ import type {DeferredUpdatesDictionary} from '@libs/actions/OnyxUpdateManager/types'; -import createProxyForValue from '@src/utils/createProxyForValue'; +import createProxyForObject from '@src/utils/createProxyForObject'; const deferredUpdatesValue = {deferredUpdates: {} as DeferredUpdatesDictionary}; -const deferredUpdatesProxy = createProxyForValue(deferredUpdatesValue); +const deferredUpdatesProxy = createProxyForObject(deferredUpdatesValue); export default deferredUpdatesProxy; diff --git a/src/libs/actions/__mocks__/App.ts b/src/libs/actions/__mocks__/App.ts index 574a5b51e1b9..3d2b5814684b 100644 --- a/src/libs/actions/__mocks__/App.ts +++ b/src/libs/actions/__mocks__/App.ts @@ -3,7 +3,7 @@ import type * as AppImport from '@libs/actions/App'; import type * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; -import createProxyForValue from '@src/utils/createProxyForValue'; +import createProxyForObject from '@src/utils/createProxyForObject'; const AppImplementation: typeof AppImport = jest.requireActual('@libs/actions/App'); const { @@ -38,7 +38,7 @@ type AppActionsMock = typeof AppImport & { const mockValues: AppMockValues = { missingOnyxUpdatesToBeApplied: undefined, }; -const mockValuesProxy = createProxyForValue(mockValues); +const mockValuesProxy = createProxyForObject(mockValues); const ApplyUpdatesImplementation: typeof ApplyUpdatesImport = jest.requireActual('@libs/actions/OnyxUpdateManager/utils/applyUpdates'); const getMissingOnyxUpdates = jest.fn((_fromID: number, toID: number) => { diff --git a/src/utils/createProxyForValue.ts b/src/utils/createProxyForObject.ts similarity index 56% rename from src/utils/createProxyForValue.ts rename to src/utils/createProxyForObject.ts index ace0951aa720..c18e5e30a0d9 100644 --- a/src/utils/createProxyForValue.ts +++ b/src/utils/createProxyForObject.ts @@ -1,4 +1,9 @@ -const createProxyForValue = <Value extends Record<string, unknown>>(value: Value) => +/** + * Creates a proxy around an object variable that can be exported from modules, to allow modification from outside the module. + * @param value the object that should be wrapped in a proxy + * @returns A proxy object that can be modified from outside the module + */ +const createProxyForObject = <Value extends Record<string, unknown>>(value: Value) => new Proxy(value, { get: (target, property) => { if (typeof property === 'symbol') { @@ -17,4 +22,4 @@ const createProxyForValue = <Value extends Record<string, unknown>>(value: Value }, }); -export default createProxyForValue; +export default createProxyForObject; From 95fc4f6c74e95c708e940918fe08e8bce65f8053 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 18:17:59 +0200 Subject: [PATCH 310/580] add isPaused checker function to SequentialQueue --- src/libs/Network/SequentialQueue.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 38b0549b28bc..b94166c0249d 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -156,6 +156,10 @@ function isRunning(): boolean { return isSequentialQueueRunning; } +function isPaused(): boolean { + return isQueuePaused; +} + // Flush the queue when the connection resumes NetworkStore.onReconnection(flush); @@ -191,4 +195,4 @@ function waitForIdle(): Promise<unknown> { return isReadyPromise; } -export {flush, getCurrentRequest, isRunning, push, waitForIdle, pause, unpause}; +export {flush, getCurrentRequest, isRunning, isPaused, push, waitForIdle, pause, unpause}; From 3e8bc28ead47b5b65c4ca6fcd0b5cc44aebbb6d0 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 18:18:11 +0200 Subject: [PATCH 311/580] reset more things before each test --- tests/unit/OnyxUpdateManagerTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/OnyxUpdateManagerTest.ts b/tests/unit/OnyxUpdateManagerTest.ts index e2bd0daec081..20b29ca18a56 100644 --- a/tests/unit/OnyxUpdateManagerTest.ts +++ b/tests/unit/OnyxUpdateManagerTest.ts @@ -36,6 +36,8 @@ describe('OnyxUpdateManager', () => { beforeEach(async () => { jest.clearAllMocks(); await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); + OnyxUpdateManagerUtils.mockValues.onValidateAndApplyDeferredUpdates = undefined; + ApplyUpdates.mockValues.onApplyUpdates = undefined; OnyxUpdateManager.resetDeferralLogicVariables(); }); From 5e42e99a6ad57a0d5ff1222f1c5891dbeff56ca2 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 18:18:38 +0200 Subject: [PATCH 312/580] add another test --- tests/actions/OnyxUpdateManagerTest.ts | 61 +++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/tests/actions/OnyxUpdateManagerTest.ts b/tests/actions/OnyxUpdateManagerTest.ts index f8a2f8fcb98f..8a4e70fb2faa 100644 --- a/tests/actions/OnyxUpdateManagerTest.ts +++ b/tests/actions/OnyxUpdateManagerTest.ts @@ -10,12 +10,12 @@ import * as OnyxUpdateManagerUtilsImport from '@libs/actions/OnyxUpdateManager/u import type {OnyxUpdateManagerUtilsMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__'; import type {ApplyUpdatesMock} from '@libs/actions/OnyxUpdateManager/utils/__mocks__/applyUpdates'; import * as ApplyUpdatesImport from '@libs/actions/OnyxUpdateManager/utils/applyUpdates'; +import * as SequentialQueue from '@libs/Network/SequentialQueue'; import CONST from '@src/CONST'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import createOnyxMockUpdate from '../utils/createOnyxMockUpdate'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('@libs/actions/App'); jest.mock('@libs/actions/OnyxUpdateManager/utils'); @@ -129,9 +129,9 @@ describe('actions/OnyxUpdateManager', () => { await Onyx.set(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, 1); await Onyx.set(ONYX_KEY, initialData); + OnyxUpdateManagerUtils.mockValues.onValidateAndApplyDeferredUpdates = undefined; App.mockValues.missingOnyxUpdatesToBeApplied = undefined; OnyxUpdateManagerExports.resetDeferralLogicVariables(); - return waitForBatchedUpdates(); }); it('should trigger Onyx update gap handling', async () => { @@ -182,16 +182,14 @@ describe('actions/OnyxUpdateManager', () => { }); OnyxUpdateManagerUtils.mockValues.onValidateAndApplyDeferredUpdates = () => { + // After the first GetMissingOnyxUpdates call has been resolved, + // we have to set the mocked results of for the second call. + App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate3, mockUpdate4]; finishFirstCall(); return Promise.resolve(); }; return firstGetMissingOnyxUpdatesCallFinished - .then(() => { - // After the first GetMissingOnyxUpdates call has been resolved, - // we have to set the mocked results of for the second call. - App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate3, mockUpdate4]; - }) .then(() => OnyxUpdateManagerExports.queryPromise) .then(() => { const expectedResult: Record<string, Partial<OnyxTypes.ReportAction>> = { @@ -223,4 +221,53 @@ describe('actions/OnyxUpdateManager', () => { expect(ApplyUpdates.applyUpdates).toHaveBeenNthCalledWith(2, {5: mockUpdate5}); }); }); + + it('should pause SequentialQueue while missing updates are being fetched', async () => { + // Since we don't want to trigger actual GetMissingOnyxUpdates calls to the server/backend, + // we have to mock the results of these calls. By setting the missingOnyxUpdatesToBeApplied + // property on the mock, we can simulate the results of the GetMissingOnyxUpdates calls. + App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate1, mockUpdate2]; + + applyOnyxUpdatesReliably(mockUpdate3); + applyOnyxUpdatesReliably(mockUpdate5); + + const assertAfterFirstGetMissingOnyxUpdates = () => { + // While the fetching of missing udpates and the validation and application of the deferred updaes is running, + // the SequentialQueue should be paused. + expect(SequentialQueue.isPaused()).toBeTruthy(); + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(1); + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(1, 1, 2); + }; + + const assertAfterSecondGetMissingOnyxUpdates = () => { + // The SequentialQueue should still be paused. + expect(SequentialQueue.isPaused()).toBeTruthy(); + expect(SequentialQueue.isRunning()).toBeFalsy(); + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + expect(App.getMissingOnyxUpdates).toHaveBeenNthCalledWith(2, 3, 4); + }; + + let firstCallFinished = false; + OnyxUpdateManagerUtils.mockValues.onValidateAndApplyDeferredUpdates = () => { + if (firstCallFinished) { + assertAfterSecondGetMissingOnyxUpdates(); + return Promise.resolve(); + } + + assertAfterFirstGetMissingOnyxUpdates(); + + // After the first GetMissingOnyxUpdates call has been resolved, + // we have to set the mocked results of for the second call. + App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate3, mockUpdate4]; + firstCallFinished = true; + return Promise.resolve(); + }; + + return OnyxUpdateManagerExports.queryPromise.then(() => { + // Once the OnyxUpdateManager has finished filling the gaps, the SequentialQueue should be unpaused again. + // It must not necessarily be running, because it might not have been flushed yet. + expect(SequentialQueue.isPaused()).toBeFalsy(); + expect(App.getMissingOnyxUpdates).toHaveBeenCalledTimes(2); + }); + }); }); From f57a149f788e0d2db56395f026b17b80671f3630 Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Tue, 23 Apr 2024 18:34:40 +0200 Subject: [PATCH 313/580] fix: first test --- tests/actions/OnyxUpdateManagerTest.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/actions/OnyxUpdateManagerTest.ts b/tests/actions/OnyxUpdateManagerTest.ts index 8a4e70fb2faa..d1a10f8a4775 100644 --- a/tests/actions/OnyxUpdateManagerTest.ts +++ b/tests/actions/OnyxUpdateManagerTest.ts @@ -141,6 +141,13 @@ describe('actions/OnyxUpdateManager', () => { App.mockValues.missingOnyxUpdatesToBeApplied = [mockUpdate2, mockUpdate3]; applyOnyxUpdatesReliably(mockUpdate2); + + // Delay all later updates, so that the update 2 has time to be written to storage and for the + // ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT to be updated. + await new Promise((resolve) => { + setTimeout(resolve, 0); + }); + applyOnyxUpdatesReliably(mockUpdate4); applyOnyxUpdatesReliably(mockUpdate3); From fb76c306fdeb752679affb9091dfad196588adc4 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:55:04 -0400 Subject: [PATCH 314/580] Delete docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md https://github.com/Expensify/Expensify/issues/385521 --- ...Expensify-Chat-Playbook-For-Conferences.md | 94 ------------------- 1 file changed, 94 deletions(-) delete mode 100644 docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md b/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md deleted file mode 100644 index caeccd1920b1..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-Playbook-For-Conferences.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Expensify Chat Playbook for Conferences -description: Best practices for how to deploy Expensify Chat for your conference -redirect_from: articles/playbooks/Expensify-Chat-Playbook-for-Conferences/ ---- -# Overview -To help make setting up Expensify Chat for your event and your attendees super simple, we’ve created a guide for all of the technical setup details. - -# Who you are -As a conference organizer, you’re expected to amaze and inspire attendees. You want attendees to get to the right place on time, engage with the speakers, and create relationships with each other that last long after the conference is done. Enter Expensify Chat, a free feature that allows attendees to interact with organizers and other attendees in realtime. With Expensify Chat, you can: - -- Communicate logistics and key information -- Foster conference wide attendee networking -- Organize conversations by topic and audience -- Continue conversations long after the event itself -- Digitize attendee social interaction -- Create an inclusive environment for virtual attendees - -Sounds good? Great! In order to ensure your team, your speakers, and your attendees have the best experience possible, we’ve created a guide on how to use Expensify Chat at your event. - -*Let’s get started!* - - -# Support -Connect with your dedicated account manager in any new.expensify.com #admins room. Your account manager is excited to brainstorm the best ways to make the most out of your event and work through any questions you have about the setup steps below. - -We also have a number of [moderation tools](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) available to admins to help make sure your event is seamless, safe, and fun! - -# Step by step instructions for setting up your conference on Expensify Chat -Based on our experience running conferences atop Expensify Chat, we recommend the following simple steps: - -## Step 1: Create your event workspace in Expensify -To create your event workspace in Expensify: -1. In [new.expensify.com](https://new.expensify.com): “+” > “New workspace” -1. Name the workspace (e.g. “ExpensiCon”) - -## Step 2: Set up all the Expensify Chat rooms you want to feature at your event -**Protip**: Your Expensify account manager can complete this step with you. Chat them in #admins on new.expensify.com to coordinate! - -To create a new chat room: -1. Go to [new.expensify.com](https://new.expensify.com) -1. Go to “+” > New room -1. Name the room (e.g. “#social”) -1. Select the workspace created in step 1 -1. Select “Public” visibility -1. Repeat for each room - -For an easy-to-follow event, we recommend creating these chat rooms: - -- **#social** - This room will include all attendees, speakers, and members of your organizing team. You can use this room to discuss social events, happy hours, dinners, or encourage attendees to mingle, share photos and connect. -- **#announcements** - This room will be used as your main announcement channel, and should only be used by organizers to announce schedule updates or anything important that your attendees need to know. Everyone in your policy will be invited to this channel, but chatting in here isn’t encouraged so to keep the noise to a minimum. -- **Create an individual room for each session** - Attendees will be able to engage with the speaker/session leader and can ask questions about their content either before/during/after the session. -- **Create a room with your Expensify account manager/s** - We can use this room to coordinate using Expensify Chat before, during, and after the event. - -**Protip** Check out our [moderation tools](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) to help flag comments deemed to be spam, inconsiderate, intimidating, bullying, harassment, assault. On any comment just click the flag icon to moderate conversation. - -## Step 3: Add chat room QR codes to the applicable session slide deck -Gather QR codes: -1. Go to [new.expensify.com](https://new.expensify.com) -1. Click into a room and click the room name or avatar in the top header -1. Go into Share Code -1. Screenshot the QR code to add to your deck - -Add the QR code to every slide so that if folks forget to scan the QR code at the beginning of the presentation, they can still join the discussion. - -## Step 4: Plan out your messaging and cadence before the event begins -Expensify Chat is a great place to provide updates leading up to your event -- share news, get folks excited about speakers, and let attendees know of crucial event information like recommended attire, travel info, and more. For example, you might consider: - -**Prep your announcements:** -- Create a document containing drafts of the key messages you intend to send throughout the day. -- If your event's agenda is broken up into hourly blocks, create a separate section for each hour of the event, to make it easy to find the correct section at the right time. -- Start each day with a review of the daily agenda, such as a bullet list summarizing what's happening hour by hour. - -**Post your updates:** -- Designate a team member to post each update in #announce at the designated time. -- Each hour, send a message listing exactly what is happening next – if there are multiple sessions happening simultaneously, list out each, along with a description of the session, a reminder of where it's located, and (most importantly) a link to the chat room for that session -- Write the messages in [markdown format](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text), such that they can be copy/pasted directly into Expensify Chat for sending. - - If there is some formatting issue upon posting, no problem: just edit the comment after sending, and it'll be fixed for everyone. -- We’d also recommend posting your updates on new lines so that if someone has a question about a certain item they can ask in a thread pertaining to that topic, rather than in one consolidated block. - -**Protip**: Your account manager can help you create this document, and would be happy to send each message at the appointed time for you. - -## Step 5: Share Expensify Chat How-To Resources with Speakers, Attendees, Admins -We’ve created a few helpful best practice docs for your speakers, admins, and attendees to help navigate using Expensify Chat at your event. Feel free to share the links below with them! - -- [Expensify Chat for Conference Attendees](https://help.expensify.com/articles/other/Expensify-Chat-For-Conference-Attendees) -- [Expensify Chat for Conference Speakers](https://help.expensify.com/articles/other/Expensify-Chat-For-Conference-Speakers) -- [Expensify Chat for Admins](https://help.expensify.com/articles/other/Expensify-Chat-For-Admins) - -## Step 6: Follow up with attendees after the event -Continue the connections by using Expensify Chat to keep your conference community connected. Encourage attendees to share photos, their favorite memories, funny stories, and more. - -# Conclusion -Once you have completed the above steps you are ready to host your conference on Expensify Chat! Let your account manager know any questions you have over in your [new.expensify.com](https://new.expensify.com) #admins room and start driving activity in your Expensify Chat rooms. Once you’ve reviewed this doc you should have the foundations in place, so a great next step is to start training your speakers on how to use Expensify Chat for their sessions. Coordinate with your account manager to make sure everything goes smoothly! From bdcaf38e0c662e8091f8a99dc1c12aaecfac97e9 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:56:38 -0400 Subject: [PATCH 315/580] Delete docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md https://github.com/Expensify/Expensify/issues/385521 --- .../Expensify-Chat-For-Conference-Speakers.md | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md deleted file mode 100644 index 652fc2ee4d2b..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Speakers.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Expensify Chat for Conference Speakers -description: Best Practices for Conference Speakers -redirect_from: articles/other/Expensify-Chat-For-Conference-Speakers/ ---- - -# Overview -Are you a speaker at an event? Great! We're delighted to provide you with an extraordinary opportunity to connect with your session attendees using Expensify Chat — before, during, and after the event. Expensify Chat offers a powerful platform for introducing yourself and your topic, fostering engaging discussions about your presentation, and maintaining the conversation with attendees even after your session is over. - -# Getting Started -We’ve rounded up some resources to get you set up on Expensify Chat and ready to start connecting with your session attendees: - -- [How to get set up and start using Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-use-chat-in-expensify) -- [How to format text in Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text) -- [How to flag content and/or users for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) - -# Setting Up a Chatroom for Your Session: Checklist -To make the most of Expensify Chat for your session, here's a handy checklist: -- Confirm that your session has an Expensify Chat room, and have the URL link ready to share with attendees in advance. - - You can find the link by clicking on the avatar for your chatroom > “Share Code” > “Copy URL to dashboard” -- Join the chat room as soon as it's ready to begin engaging with your audience right from the start. -- Consider having a session moderator with you on the day to assist with questions and discussions while you're presenting. -- Include the QR code for your session's chat room in your presentation slides. Displaying it prominently on every slide ensures that attendees can easily join the chat throughout your presentation. - -# Tips to Enhance Engagement Around Your Session -By following these steps and utilizing Expensify Chat, you can elevate your session to promote valuable interactions with your audience, and leave a lasting impact beyond the conference. We can't wait to see your sessions thrive with the power of Expensify Chat! - -**Before the event:** -- Share your session's QR code or URL on your social media platforms, your website or other platforms to encourage attendees to join the conversation early on. -- Encourage attendees to ask questions in the chat room before the event, enabling you to tailor your session and address their specific interests. - -**During the event:** -- Keep your QR code readily available during the conference by saving it as a photo on your phone or setting it as your locked screen image. This way, you can easily share it with others you meet. -- Guide your audience back to the QR code and encourage them to ask questions, fostering interactive discussions. - -**After the event:** -- Continue engaging with attendees by responding to their questions and comments, helping you expand your audience and sustain interest. -- Share your presentation slides after the event as well as any photos from your session, allowing attendees to review and share your content with their networks if they want to. - -If you have any questions on how Expensify Chat works, head to our guide [here](https://help.expensify.com/articles/other/Everything-About-Chat). From 8868b66c734f4d3eb764020ad7e4f7be2ffc04dc Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:57:50 -0400 Subject: [PATCH 316/580] Delete docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md https://github.com/Expensify/Expensify/issues/385521 --- ...Expensify-Chat-For-Conference-Attendees.md | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md b/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md deleted file mode 100644 index 30eeb4158902..000000000000 --- a/docs/articles/new-expensify/chat/Expensify-Chat-For-Conference-Attendees.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: Expensify Chat for Conference Attendees -description: Best Practices for Conference Attendees -redirect_from: articles/other/Expensify-Chat-For-Conference-Attendees/ ---- - -# Overview -Expensify Chat is the best way to meet and network with other event attendees. No more hunting down your contacts by walking the floor or trying to find someone in crowds at a party. Instead, you can use Expensify Chat to network and collaborate with others throughout the conference. - -To help get you set up for a great event, we’ve created a guide to help you get the most out of using Expensify Chat at the event you’re attending. - -# Getting Started -We’ve rounded up some resources to get you set up on Expensify Chat and ready to start connecting with your fellow attendees: - -- [How to get set up and start using Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-use-chat-in-expensify) -- [How to format text in Expensify Chat](https://help.expensify.com/articles/other/Everything-About-Chat#how-to-format-text) -- [How to flag content and/or users for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) - -# Chat Best Practices -To get the most out of your experience at your conference and engage people in a meaningful conversation that will fulfill your goals instead of turning people off, here are some tips on what to do and not to do as an event attendee using Expensify Chat: - -**Do:** -- Chat about non-business topics like where the best coffee is around the event, what great lunch options are available, or where the parties are happening that night! -- Share pictures of your travel before the event to hype everyone up, during the event if you met that person you’ve been meaning to see for years, or a fun pic from a party. -- Try to create fun groups with your fellow attendees around common interests like touring a local sight, going for a morning run, or trying a famous restaurant. - -**Don't:** -- Pitch your services in public rooms like #social or speaking session rooms. -- Start a first message with a stranger with a sales pitch. -- Discuss controversial topics such as politics, religion, or anything you wouldn’t say on a first date. -- In general just remember that you are still here for business, your profile is public, and you’re representing yourself & company, so do not say anything you wouldn’t feel comfortable sharing in a business setting. - -**Pro-Tips:** -Get active in Chat early and often by having real conversations around thought leadership or non-business discussions to stand out from the crowd! Also if you’re in a session and are afraid to ask a question, just ask in the chat room to make sure you can discuss it with the speaker after the session ends. - -By following these tips you’ll ensure that your messages will not be [flagged for moderation](https://help.expensify.com/articles/other/Everything-About-Chat#flagging-content-as-offensive) and you will not mess it up for the rest of us. From 1ae1c5b929bfda75b6e2bb57eecebbb99e922bd5 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Wed, 24 Apr 2024 00:30:24 +0530 Subject: [PATCH 317/580] show audit status only when receipt is scanned. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 3 ++- src/libs/TransactionUtils.ts | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 00014707e5df..141cdcebf4b8 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -156,6 +156,7 @@ function MoneyRequestView({ const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); + const isReceiptScanCompleted = hasReceipt && TransactionUtils.isReceiptScanCompleted(transaction); const isAdmin = policy?.role === 'admin'; const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; @@ -330,7 +331,7 @@ function MoneyRequestView({ <View style={shouldShowAnimatedBackground && [StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth, true)]}> <ReceiptAuditHeader notes={noteTypeViolations} - showAuditMessage={shouldShowNotesViolations} + showAuditMessage={shouldShowNotesViolations && isReceiptScanCompleted} /> {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {(showMapAsImage || hasReceipt) && ( diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index d6fec8d7344e..2ecb58dd4ba7 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -491,6 +491,10 @@ function isReceiptBeingScanned(transaction: OnyxEntry<Transaction>): boolean { return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction?.receipt?.state); } +function isReceiptScanCompleted(transaction: OnyxEntry<Transaction>): boolean { + return [CONST.IOU.RECEIPT_STATE.SCANCOMPLETE].some((value) => value === transaction?.receipt?.state); +} + /** * Check if the transaction has a non-smartscanning receipt and is missing required fields */ @@ -687,6 +691,7 @@ export { hasEReceipt, hasRoute, isReceiptBeingScanned, + isReceiptScanCompleted, getValidWaypoints, isDistanceRequest, isFetchingWaypointsFromServer, From b56b7ed5136c0ac9e471d805313d28e65d416ac6 Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Tue, 23 Apr 2024 17:18:28 -0700 Subject: [PATCH 318/580] Rename Close-or-reopen-account.md to Close-or-reopen-account.md Updated "account settings" to "account-settings" as requested by @rushatgabhane. --- .../Close-or-reopen-account.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/settings/{account settings => account-settings}/Close-or-reopen-account.md (100%) diff --git a/docs/articles/expensify-classic/settings/account settings/Close-or-reopen-account.md b/docs/articles/expensify-classic/settings/account-settings/Close-or-reopen-account.md similarity index 100% rename from docs/articles/expensify-classic/settings/account settings/Close-or-reopen-account.md rename to docs/articles/expensify-classic/settings/account-settings/Close-or-reopen-account.md From a79b26734af7d423a72150115e030595079dfc9f Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Tue, 23 Apr 2024 17:19:33 -0700 Subject: [PATCH 319/580] Rename Set-Notifications.md to Set-Notifications.md Updated "account settings" to "account-settings" per @rushatgabhane request. --- .../{account settings => account-settings}/Set-Notifications.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/settings/{account settings => account-settings}/Set-Notifications.md (100%) diff --git a/docs/articles/expensify-classic/settings/account settings/Set-Notifications.md b/docs/articles/expensify-classic/settings/account-settings/Set-Notifications.md similarity index 100% rename from docs/articles/expensify-classic/settings/account settings/Set-Notifications.md rename to docs/articles/expensify-classic/settings/account-settings/Set-Notifications.md From 8d6d0c1e16d1f08e58d2103c455c9d192ae9d94b Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Tue, 23 Apr 2024 18:39:35 -0700 Subject: [PATCH 320/580] Update redirects.csv Adding two redirects for Classic > Workspace docs. https://github.com/Expensify/App/issues/39202 --- docs/redirects.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 95404c2326a0..a16815860af5 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -156,6 +156,8 @@ https://help.expensify.com/articles/expensify-classic/send-payments/Reimbursing- https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/SAML-SSO,https://help.expensify.com/articles/expensify-classic/settings/Enable-two-factor-authentication https://help.expensify.com/articles/expensify-classic/workspaces/Budgets,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets https://help.expensify.com/articles/expensify-classic/workspaces/Categories,https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories +https://help.expensify.com/articles/expensify-classic/expenses/Per-Diem-Expenses.html,https://help.expensify.com/articles/expensify-classic/workspaces/Enable-per-diem-expenses +https://help.expensify.com/articles/expensify-classic/workspaces/Budgets.html,https://help.expensify.com/articles/expensify-classic/workspaces/Set-budgets https://help.expensify.com/articles/expensify-classic/workspaces/Tags,https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags https://help.expensify.com/expensify-classic/hubs/manage-employees-and-report-approvals,https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Approval-Workflows https://help.expensify.com/articles/expensify-classic/reports/Report-Audit-Log-and-Comments,https://help.expensify.com/articles/expensify-classic/reports/Print-or-download-a-report From c4cf012113020d11bf56dc047108195fde57ed21 Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Tue, 23 Apr 2024 18:45:28 -0700 Subject: [PATCH 321/580] Rename tax-tracking.md to Tax-Tracking.md Updated the case of the title. https://github.com/Expensify/Expensify/issues/381301#issuecomment-2021376947 --- .../workspaces/{tax-tracking.md => Tax-Tracking.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/workspaces/{tax-tracking.md => Tax-Tracking.md} (100%) diff --git a/docs/articles/expensify-classic/workspaces/tax-tracking.md b/docs/articles/expensify-classic/workspaces/Tax-Tracking.md similarity index 100% rename from docs/articles/expensify-classic/workspaces/tax-tracking.md rename to docs/articles/expensify-classic/workspaces/Tax-Tracking.md From 6f5a1d4778a8cdb3abf280757846052cae2aca66 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Wed, 24 Apr 2024 11:40:34 +0800 Subject: [PATCH 322/580] clear plaid event when mounted --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 22f9afbc16b4..edd2155a3c55 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -243,6 +243,7 @@ function ReimbursementAccountPage({ const isStepToOpenEmpty = getStepToOpenFromRouteParams(route) === ''; if (isStepToOpenEmpty) { BankAccounts.setBankAccountSubStep(null); + BankAccounts.setPlaidEvent(null); } fetchData(false, isStepToOpenEmpty); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -388,6 +389,7 @@ function ReimbursementAccountPage({ ].some((value) => value === currentStep) ); + console.log('is loading?', {isLoading, isReimbursementAccountLoading, hasACHDataBeenLoaded, plaidCurrentEvent, substep: achData?.subStep}) // Show loading indicator when page is first time being opened and props.reimbursementAccount yet to be loaded from the server // or when data is being loaded. Don't show the loading indicator if we're offline and restarted the bank account setup process // On Android, when we open the app from the background, Onfido activity gets destroyed, so we need to reopen it. From defac609fb75142731243c6a019ab21c3a14f7ee Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Wed, 24 Apr 2024 11:27:15 +0700 Subject: [PATCH 323/580] fix lint --- src/components/MoneyRequestConfirmationList.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 2a290832092d..185972e1b596 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -695,7 +695,9 @@ function MoneyRequestConfirmationList({ title={payeePersonalDetails.displayName ?? ReportUtils.getDisplayNameForParticipant(payeePersonalDetails.accountID)} icon={payeeIcons} onPress={() => { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams())); + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), + ); }} shouldShowRightIcon={!isPolicyExpenseChat && !isReadOnly} titleWithTooltips={payeePersonalDetails?.isOptimisticPersonalDetail ? undefined : payeeTooltipDetails} From 4505305da56893460a4db816c8fe2b4b471f6468 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Wed, 24 Apr 2024 15:02:26 +0700 Subject: [PATCH 324/580] fix: Name RHP stays open while not found view is loading --- src/pages/workspace/WorkspaceNamePage.tsx | 67 ++++++++++++----------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/src/pages/workspace/WorkspaceNamePage.tsx b/src/pages/workspace/WorkspaceNamePage.tsx index dc2f8ebb4560..96c045332271 100644 --- a/src/pages/workspace/WorkspaceNamePage.tsx +++ b/src/pages/workspace/WorkspaceNamePage.tsx @@ -15,6 +15,7 @@ import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WorkspaceSettingsForm'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import withPolicy from './withPolicy'; import type {WithPolicyProps} from './withPolicy'; @@ -53,39 +54,41 @@ function WorkspaceNamePage({policy}: Props) { }, []); return ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceNamePage.displayName} - > - <HeaderWithBackButton - title={translate('workspace.editor.nameInputLabel')} - onBackButtonPress={() => Navigation.goBack()} - /> - - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM} - submitButtonText={translate('workspace.editor.save')} - style={[styles.flexGrow1, styles.ph5]} - scrollContextEnabled - validate={validate} - onSubmit={submit} - enabledWhenOffline + <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''}> + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceNamePage.displayName} > - <View style={styles.mb4}> - <InputWrapper - InputComponent={TextInput} - role={CONST.ROLE.PRESENTATION} - inputID={INPUT_IDS.NAME} - label={translate('workspace.editor.nameInputLabel')} - accessibilityLabel={translate('workspace.editor.nameInputLabel')} - defaultValue={policy?.name} - spellCheck={false} - autoFocus - /> - </View> - </FormProvider> - </ScreenWrapper> + <HeaderWithBackButton + title={translate('workspace.editor.nameInputLabel')} + onBackButtonPress={() => Navigation.goBack()} + /> + + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM} + submitButtonText={translate('workspace.editor.save')} + style={[styles.flexGrow1, styles.ph5]} + scrollContextEnabled + validate={validate} + onSubmit={submit} + enabledWhenOffline + > + <View style={styles.mb4}> + <InputWrapper + InputComponent={TextInput} + role={CONST.ROLE.PRESENTATION} + inputID={INPUT_IDS.NAME} + label={translate('workspace.editor.nameInputLabel')} + accessibilityLabel={translate('workspace.editor.nameInputLabel')} + defaultValue={policy?.name} + spellCheck={false} + autoFocus + /> + </View> + </FormProvider> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> ); } From 2c9ab1ea77929d20d8b28174fb2af0d7c73aef3c Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Wed, 24 Apr 2024 16:18:35 +0530 Subject: [PATCH 325/580] fix: disabled receipt cursor style. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptEmptyState.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReceiptEmptyState.tsx b/src/components/ReceiptEmptyState.tsx index abc025814096..71d64c7483f1 100644 --- a/src/components/ReceiptEmptyState.tsx +++ b/src/components/ReceiptEmptyState.tsx @@ -27,6 +27,7 @@ function ReceiptEmptyState({hasError = false, onPress = () => {}, disabled = fal accessibilityLabel={translate('receipt.upload')} onPress={onPress} disabled={disabled} + disabledStyle={styles.cursorDefault} style={[styles.alignItemsCenter, styles.justifyContentCenter, styles.moneyRequestViewImage, styles.moneyRequestAttachReceipt, hasError && styles.borderColorDanger]} > <Icon From 26aed7a81fe954615e2e7d7864f36b25e7f5fc18 Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Wed, 24 Apr 2024 13:38:19 +0200 Subject: [PATCH 326/580] review fixes wip --- .../workspace/AccessOrNotFoundWrapper.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 29896eb5b6d0..eb3114eaafae 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -16,12 +16,11 @@ import callOrReturn from '@src/types/utils/callOrReturn'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; const POLICY_ACCESS_VARIANTS = { - PAID: (policy: OnyxEntry<OnyxTypes.Policy>) => !PolicyUtils.isPaidGroupPolicy(policy) || !policy?.isPolicyExpenseChatEnabled, - ADMIN: (policy: OnyxEntry<OnyxTypes.Policy>) => !PolicyUtils.isPolicyAdmin(policy), + PAID: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isPaidGroupPolicy(policy) && !!policy?.isPolicyExpenseChatEnabled, + ADMIN: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isPolicyAdmin(policy), } as const satisfies Record<string, (policy: OnyxTypes.Policy) => boolean>; type PolicyAccessVariant = keyof typeof POLICY_ACCESS_VARIANTS; - type AccessOrNotFoundWrapperOnyxProps = { /** The report currently being looked at */ policy: OnyxEntry<OnyxTypes.Policy>; @@ -44,10 +43,10 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { featureName?: PolicyFeatureName; }; -type PageNotFoundFallackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID'> & {showFullScreenFallback: boolean}; +type PageNotFoundFallackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID'> & {shouldShowFullScreenFallback: boolean}; -function PageNotFoundFallback({policyID, showFullScreenFallback}: PageNotFoundFallackProps) { - return showFullScreenFallback ? ( +function PageNotFoundFallback({policyID, shouldShowFullScreenFallback}: PageNotFoundFallackProps) { + return shouldShowFullScreenFallback ? ( <FullPageNotFoundView shouldShow onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} @@ -76,12 +75,13 @@ function AccessOrNotFoundWrapper({accessVariants = [], ...props}: AccessOrNotFou const shouldShowFullScreenLoadingIndicator = isLoadingReportData !== false && (!Object.entries(policy ?? {}).length || !policy?.id); const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true; - const pageUnaccessible = accessVariants.reduce((acc, variant) => { + + const isPageAccessible = accessVariants.reduce((acc, variant) => { const accessFunction = POLICY_ACCESS_VARIANTS[variant]; - return acc || accessFunction(policy); - }, false); + return acc && accessFunction(policy); + }, true); - const shouldShowNotFoundPage = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id || pageUnaccessible || !isFeatureEnabled; + const shouldShowNotFoundPage = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id || !isPageAccessible || !isFeatureEnabled; if (shouldShowFullScreenLoadingIndicator) { return <FullscreenLoadingIndicator />; @@ -91,7 +91,7 @@ function AccessOrNotFoundWrapper({accessVariants = [], ...props}: AccessOrNotFou return ( <PageNotFoundFallback policyID={policyID} - showFullScreenFallback={!isFeatureEnabled} + shouldShowFullScreenFallback={!isFeatureEnabled} /> ); } From 0dfd073fd96b7ab211ae064e1671e8822b7f5ef3 Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Wed, 24 Apr 2024 19:18:38 +0700 Subject: [PATCH 327/580] fix update split expense icon in chat --- .../ReportActionCompose/AttachmentPickerWithMenuItems.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index e2d87d55605a..08313ab652d4 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -125,7 +125,7 @@ function AttachmentPickerWithMenuItems({ const moneyRequestOptions = useMemo(() => { const options: MoneyRequestOptions = { [CONST.IOU.TYPE.SPLIT]: { - icon: Expensicons.Receipt, + icon: Expensicons.Transfer, text: translate('iou.splitExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SPLIT, report?.reportID ?? ''), }, From c15fd3a3ba815443583ecdf14b070b746cfea695 Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Wed, 24 Apr 2024 19:31:28 +0700 Subject: [PATCH 328/580] fix type --- src/libs/getIconForAction/index.ts | 2 +- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/getIconForAction/index.ts b/src/libs/getIconForAction/index.ts index 963c92640f39..820cf2687f02 100644 --- a/src/libs/getIconForAction/index.ts +++ b/src/libs/getIconForAction/index.ts @@ -4,7 +4,7 @@ import CONST from '@src/CONST'; const getIconForAction = (actionType: ValueOf<typeof CONST.IOU.TYPE>) => { switch (actionType) { - case CONST.IOU.TYPE.TRACK_EXPENSE: + case CONST.IOU.TYPE.TRACK: return Expensicons.Coins; case CONST.IOU.TYPE.REQUEST: return Expensicons.Receipt; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 3425942658c6..be0c61f822a5 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -100,7 +100,7 @@ const getQuickActionIcon = (action: QuickActionName): React.FC<SvgProps> => { case CONST.QUICK_ACTIONS.TRACK_DISTANCE: case CONST.QUICK_ACTIONS.TRACK_MANUAL: case CONST.QUICK_ACTIONS.TRACK_SCAN: - return getIconForAction(CONST.IOU.TYPE.TRACK_EXPENSE); + return getIconForAction(CONST.IOU.TYPE.TRACK); default: return Expensicons.MoneyCircle; } @@ -295,7 +295,7 @@ function FloatingActionButtonAndPopover( ...(canUseTrackExpense && selfDMReportID ? [ { - icon: getIconForAction(CONST.IOU.TYPE.TRACK_EXPENSE), + icon: getIconForAction(CONST.IOU.TYPE.TRACK), text: translate('iou.trackExpense'), onSelected: () => interceptAnonymousUser(() => From 14a545979612f1df5eb95153d920f06751f79601 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 07:52:57 -0500 Subject: [PATCH 329/580] Create Leave-a-chat-room.md New article --- .../new-expensify/chat/Leave-a-chat-room.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/articles/new-expensify/chat/Leave-a-chat-room.md diff --git a/docs/articles/new-expensify/chat/Leave-a-chat-room.md b/docs/articles/new-expensify/chat/Leave-a-chat-room.md new file mode 100644 index 000000000000..252e7e94f1ac --- /dev/null +++ b/docs/articles/new-expensify/chat/Leave-a-chat-room.md @@ -0,0 +1,23 @@ +--- +title: Leave a chat room +description: Remove a chat room from your inbox +--- +<div id="new-expensify" markdown="1"> + +If you wish to no longer be part of a chat room, you can leave the room. This means that the chat room will no longer be visible in your inbox, and you will no longer see updates posted to the room or be notified of new messages. + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat room. +2. Click the 3 dot menu icon in the top right and select **Leave**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat room. +2. Tap the 3 dot menu icon in the top right and select **Leave**. +{% include end-option.html %} + +{% include end-selector.html %} + +</div> From d88c4e7c3dacb1d62df03646e10d4a4c0e2fb637 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 24 Apr 2024 15:01:49 +0200 Subject: [PATCH 330/580] fix: jumping inputs --- .../EnablePayments/PersonalInfo/substeps/FullNameStep.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx index 94ddb359e167..2f0926dcdefb 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx @@ -59,7 +59,7 @@ function FullNameStep({onNext, isEditing}: SubStepProps) { > <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb6]}>{translate('personalInfoStep.whatsYourLegalName')}</Text> - <View style={[styles.flex2, styles.mb6]}> + <View style={[styles.mb6]}> <InputWrapper InputComponent={TextInput} inputID={PERSONAL_INFO_STEP_KEY.FIRST_NAME} @@ -70,7 +70,7 @@ function FullNameStep({onNext, isEditing}: SubStepProps) { shouldSaveDraft={!isEditing} /> </View> - <View style={[styles.flex2, styles.mb6]}> + <View style={[styles.mb6]}> <InputWrapper InputComponent={TextInput} inputID={PERSONAL_INFO_STEP_KEY.LAST_NAME} From 6fd845116d180e6d2585b8565d4847fcd36f3373 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 08:08:47 -0500 Subject: [PATCH 331/580] Create Send-and-format-chat-messages.md New article --- .../chat/Send-and-format-chat-messages.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/articles/new-expensify/chat/Send-and-format-chat-messages.md diff --git a/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md b/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md new file mode 100644 index 000000000000..edef142a80bf --- /dev/null +++ b/docs/articles/new-expensify/chat/Send-and-format-chat-messages.md @@ -0,0 +1,49 @@ +--- +title: Send and format chat messages +description: Send chat messages and stylize them with markdown +--- +<div id="new-expensify" markdown="1"> + +Once you are added to a chat or create a new chat, you can send messages to other members in the chat and even format the text to include bold, italics, and more. + +{% include info.html %} +Some chat rooms may have permissions that restrict who can send messages. In this case, you won’t be able to send messages in the room if you do not have the required permission level. +{% include end-info.html %} + +To send a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click any chat in your inbox to open it. +2. Use the message bar at the bottom of the message to enter a message, add attachments, and add emojis. + - **To add a message**: Click the field labeled “Write something” and type a message. + - **To add an attachment**: Click the plus icon and select **Add attachment**. Then choose the attachment from your files. + - **To add an emoji**: Click the emoji icon to the right of the message field. +3. Press Enter on your keyboard or click the Send icon to send the message. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap any chat in your inbox to open it. +2. Use the message bar at the bottom of the message to enter a message, add attachments, and add emojis. + - **To add a message**: Tap the field labeled “Write something” and type a message. + - **To add an attachment**: Tap the plus icon and select **Add attachment**. Then choose the attachment from your files. + - **To add an emoji**: Tap the emoji icon to the right of the message field. +3. Tap the Send icon to send the message. +{% include end-option.html %} + +{% include end-selector.html %} + +# Format text in a chat message + +You can format the text in a chat message using markdown. + +- _Italicize_: Add an underscore _ on both sides of the text. +- **Bold**: Add two asterisks ** on both sides of the text. +- ~~Strikethrough~~: Add two tildes ~~ on both sides of the text. +- Heading: Add a number sign # in front of the text. +- > Blockquote: Add an angled bracket > in front of the text. +- `Code block for a small amount of text`: Add a backtick ` on both sides of the text. +- Code block for the entire message: Add three backticks ``` at the beginning and the end of the message. + +</div> From e96d59d478893f7243c150e17fd29f8d0c5154a2 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski <julian.kobrynski@callstack.com> Date: Wed, 24 Apr 2024 15:11:05 +0200 Subject: [PATCH 332/580] add delay to promise while, log poll rate --- .../javascript/awaitStagingDeploys/awaitStagingDeploys.ts | 1 + .github/actions/javascript/awaitStagingDeploys/index.js | 8 +++++++- .github/libs/promiseWhile.ts | 7 ++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts index 7de78e257dc4..26947193cd80 100644 --- a/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts +++ b/.github/actions/javascript/awaitStagingDeploys/awaitStagingDeploys.ts @@ -8,6 +8,7 @@ import {promiseDoWhile} from '@github/libs/promiseWhile'; type CurrentStagingDeploys = Awaited<ReturnType<typeof GitHubUtils.octokit.actions.listWorkflowRuns>>['data']['workflow_runs']; function run() { + console.info('[awaitStagingDeploys] POLL RATE', CONST.POLL_RATE); console.info('[awaitStagingDeploys] run()'); console.info('[awaitStagingDeploys] getStringInput', getStringInput); console.info('[awaitStagingDeploys] GitHubUtils', GitHubUtils); diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index d84c6df1a0d3..c91313520673 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -12131,6 +12131,7 @@ const CONST_1 = __importDefault(__nccwpck_require__(9873)); const GithubUtils_1 = __importDefault(__nccwpck_require__(9296)); const promiseWhile_1 = __nccwpck_require__(9438); function run() { + console.info('[awaitStagingDeploys] POLL RATE', CONST_1.default.POLL_RATE); console.info('[awaitStagingDeploys] run()'); console.info('[awaitStagingDeploys] getStringInput', ActionUtils_1.getStringInput); console.info('[awaitStagingDeploys] GitHubUtils', GithubUtils_1.default); @@ -12742,7 +12743,12 @@ function promiseWhile(condition, action) { resolve(); return; } - Promise.resolve(actionResult).then(loop).catch(reject); + Promise.resolve(actionResult) + .then(() => { + // Set a timeout to delay the next loop iteration + setTimeout(loop, 1000); // 1000 ms delay + }) + .catch(reject); } }; loop(); diff --git a/.github/libs/promiseWhile.ts b/.github/libs/promiseWhile.ts index 01c061096d64..401b6ee2e18a 100644 --- a/.github/libs/promiseWhile.ts +++ b/.github/libs/promiseWhile.ts @@ -19,7 +19,12 @@ function promiseWhile(condition: () => boolean, action: (() => Promise<void>) | return; } - Promise.resolve(actionResult).then(loop).catch(reject); + Promise.resolve(actionResult) + .then(() => { + // Set a timeout to delay the next loop iteration + setTimeout(loop, 1000); // 1000 ms delay + }) + .catch(reject); } }; loop(); From 1f1ad8815a8841398613d59a6ece9dea8e194379 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 08:17:20 -0500 Subject: [PATCH 333/580] Create Start-a-conversation-thread.md New article --- .../chat/Start-a-conversation-thread.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/articles/new-expensify/chat/Start-a-conversation-thread.md diff --git a/docs/articles/new-expensify/chat/Start-a-conversation-thread.md b/docs/articles/new-expensify/chat/Start-a-conversation-thread.md new file mode 100644 index 000000000000..cb3a3aa69296 --- /dev/null +++ b/docs/articles/new-expensify/chat/Start-a-conversation-thread.md @@ -0,0 +1,33 @@ +--- +title: Start a conversation thread +description: Start a private conversation related to a different message +--- +<div id="new-expensify" markdown="1"> + +You can respond directly to a message sent in a chat group or room to start a private 1-on-1 chat with another member about the message (instead of replying to the entire group or room). + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat in your inbox. +2. Right-click the message and select **Reply in thread**. +3. Enter and submit your reply in the new chat. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat in your inbox. +2. Press and hold the message and select **Reply in thread**. +3. Enter and submit your reply in the new chat. +{% include end-option.html %} + +{% include end-selector.html %} + +To return to the conversation where the thread originated from, you can click the link at the top of the thread. + +</div> + + + + + + From fcfafecd4b32a2ce6ebc787cad531c8706c5277d Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 08:27:31 -0500 Subject: [PATCH 334/580] Create Reorder-chat-inbox.md New article --- .../new-expensify/chat/Reorder-chat-inbox.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 docs/articles/new-expensify/chat/Reorder-chat-inbox.md diff --git a/docs/articles/new-expensify/chat/Reorder-chat-inbox.md b/docs/articles/new-expensify/chat/Reorder-chat-inbox.md new file mode 100644 index 000000000000..cd62f95e63e8 --- /dev/null +++ b/docs/articles/new-expensify/chat/Reorder-chat-inbox.md @@ -0,0 +1,49 @@ +--- +title: Reorder chat inbox +description: Change how your chats are displayed in your inbox +--- +<div id="new-expensify" markdown="1"> + +You can customize the order of the chat messages in your inbox by pinning them to the top and/or changing your message priority to Most Recent or #focus: +- **Pin**: Bumps a specific chat up to the top of your inbox list. +- **Message priority**: Determines the order that messages are sorted and displayed: + - **Most Recent**: Displays all chats by default sorted by the most recent, and keep your pinned chats at the top of the list. + - **#focus**: Displays only unread and pinned chats sorted alphabetically. + +# Pin a message + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +Right-click a chat in your inbox and select **Pin**. The chat will now be pinned to the top of your inbox above all of the others. + +To unpin a chat, repeat this process to click the pin icon again to remove it. +{% include end-option.html %} + +{% include option.html value="mobile" %} +Press and hold a chat in your inbox and select **Pin**. The chat will now be pinned to the top of your inbox above all of the others. + +To unpin a chat, repeat this process to tap the pin icon again to remove it. +{% include end-option.html %} + +{% include end-selector.html %} + +# Change message priority + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click your profile image or icon in the bottom left menu. +2. Click the **Preferences** tab on the left. +3. Click **Priority Mode** to select either #focus or Most recent. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap your profile image or icon in the bottom menu. +2. Tap the **Preferences** tab. +3. Tap **Priority Mode** to select either #focus or Most recent. +{% include end-option.html %} + +{% include end-selector.html %} + +</div> From d7cfad7ad810abea03c11403417fe1bca916b082 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 08:53:42 -0500 Subject: [PATCH 335/580] Update and rename Introducing-Expensify-Chat.md to Create-a-new-chat.md Article rewrite to focus only on creating a new chat. Other topics from this article were split into their own articles --- .../new-expensify/chat/Create-a-new-chat.md | 112 ++++++++++++ .../chat/Introducing-Expensify-Chat.md | 165 ------------------ 2 files changed, 112 insertions(+), 165 deletions(-) create mode 100644 docs/articles/new-expensify/chat/Create-a-new-chat.md delete mode 100644 docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md diff --git a/docs/articles/new-expensify/chat/Create-a-new-chat.md b/docs/articles/new-expensify/chat/Create-a-new-chat.md new file mode 100644 index 000000000000..db76946b3d83 --- /dev/null +++ b/docs/articles/new-expensify/chat/Create-a-new-chat.md @@ -0,0 +1,112 @@ +--- +title: Create a new chat +description: Start a new private, group, or room chat +redirect_from: articles/other/Everything-About-Chat/ +--- +<div id="new-expensify" markdown="1"> + +Expensify Chat is an instant messaging system that helps you converse with people both inside and outside of your workspace about payments, company updates, and more. Expensify Chats are held in private chats, groups, and rooms. +- **Private chats**: Private conversations for 1-on-1 chats +- **Groups**: Private conversations for 2+ participants +- **Rooms**: Public conversations that are available for all members of your workspace + +# Start a private 1-on-1 chat + +{% include info.html %} +You cannot add more people to a private chat. If later you wish to add more people to the conversation, you’ll need to create a group chat. +{% include end-info.html %} + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and click their name to start a new chat with them. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and tap their name to start a new chat with them. +{% include end-option.html %} + +{% include end-selector.html %} + +# Start a group chat + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and click **Add to group**. Repeat this step until all desired participants are added. *Note: Group participants are listed with a green checkmark.* +3. Click **Next**. +4. Update the group image or name. + - **Name**: Click **Group Name** and enter a new name. Then click **Save**. + - **Image**: Click the profile image and select **Upload Image**. Then choose a new image from your computer files and select the desired image zoom. +5. Click **Start group**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Enter the name, email, or phone number for the person you want to chat with and tap **Add to group**. Repeat this step until all desired participants are added. *Note: Group participants are listed with a green checkmark.* +3. Tap **Next** (or **Create chat** if you add only one person to the group). +4. Update the group image or name. + - **Name**: Tap **Group Name** and enter a new name. Then tap **Save**. + - **Image**: Tap the profile image and select **Upload Image**. Then choose a new image from your photos and select the desired image zoom. +5. Tap **Start group**. + +{% include end-option.html %} + +{% include end-selector.html %} + +# Start a chat room + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + button in the bottom left menu and select **Start Chat**. +2. Click the #Room tab at the top. +3. Enter a name for the room. *Note: It cannot be the same as another room in the Workspace.* +4. (Optional) Add a description of the room. +5. Click **Workspace** to select the workspace for the room. +6. Click **Who can post** to determine if all members can post or only Admins. +7. Click **Visibility** to determine who can find the room. + - **Public**: Anyone can find the room (perfect for conferences). + - **Private**: Only people explicitly invited can find the room. + - **Workspace**: Only workspace members can find the room. + +{% include info.html %} +Anyone, including non-Workspace Members, can be invited to a private or restricted room. +{% include end-info.html %} + +8. Click **Create room**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + button in the bottom menu and select **Start Chat**. +2. Tap the #Room tab at the top. +3. Enter a name for the room. *Note: It cannot be the same as another room in the Workspace.* +4. (Optional) Add a description of the room. +5. Tap **Workspace** to select the workspace for the room. +6. Tap **Who can post** to determine if all members can post or only Admins. +7. Tap **Visibility** to determine who can find the room. + - **Public**: Anyone can find the room (perfect for conferences). + - **Private**: Only people explicitly invited can find the room. + - **Workspace**: Only workspace members can find the room. + +{% include info.html %} +Anyone, including non-Workspace Members, can be invited to a private or restricted room. +{% include end-info.html %} + +8. Tap **Create room**. +{% include end-option.html %} + +{% include end-selector.html %} + +# FAQs + +**What's the difference between a private 1-on-1 chat and a group chat with only 2 people?** +With a group chat, you can add additional people to the chat at any time. But you cannot add additional people to a private 1-on-1 chat. +</div> + + + + diff --git a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md deleted file mode 100644 index 096a3d1527be..000000000000 --- a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: Introducing Expensify Chat -description: Everything you need to know about Expensify Chat! -redirect_from: articles/other/Everything-About-Chat/ ---- - -<!-- The lines above are required by Jekyll to process the .md file --> - -# Overview - -For a quick snapshot of how Expensify Chat works, and New Expensify in general, check out our website! - -# What’s Expensify Chat? - -Expensify Chat is an instant messaging and payment platform. You can manage all your payments, whether for business or personal, and discuss the transactions themselves. - -With Expensify Chat, you can start a conversation about that missing receipt your employee forgot to submit or chat about splitting that electric bill with your roommates. Expensify makes sending and receiving money as easy as sending and receiving messages. Chat with anyone directly, in groups, or in rooms. - -# Getting started - -Download New Expensify from the [App Store](https://apps.apple.com/us/app/expensify-cash/id1530278510) or [Google Play](https://play.google.com/store/apps/details?id=com.expensify.chat) to use the chat function. You can also access your account at new.expensify.com from your favorite web browser. - -After downloading the app, log into your new.expensify.com account (you’ll use the same login information as your Expensify Classic account). From there, you can customize your profile and start chatting. - -## How to send messages - -1. Click **+** then **Send message** in New Expensify -2. Choose **Chat** -3. Search for any name, email or phone number -4. Select the individual to begin chatting - -## How to create a group - -1. Click **+**, then **Send message** in New Expensify -2. Search for any name, email or phone number -3. Click **Add to group** -4. Group participants are listed with a green check -5. Repeat steps 1-3 to add more participants to the group -6. Click **Create chat** to start chatting - -## How to create a room - -1. Click **+**, then **Send message** in New Expensify -2. Click **Room** -3. Enter a room name that doesn’t already exist on the intended Workspace -4. Choose the Workspace you want to associate the room with. -5. Choose the room’s visibility setting: -6. Private: Only people explicitly invited can find the room* -7. Restricted: Workspace members can find the room* -8. Public: Anyone can find the room - -*Anyone, including non-Workspace Members, can be invited to a Private or Restricted room. - -## How to invite and remove members - -You can invite people to a Group or Room by @mentioning them or from the Members pane. - -## Mentions: - -1. Type **@** and start typing the person’s name or email address -2. Choose one or more contacts -3. Input message, if desired, then send - - -## Members pane invites: - -1. Click the **Room** or **Group** header -2. Select **Members** -3. Click **Invite** -4. Find and select any contact/s you’d like to invite -5. Click **Next** -6. Write a custom invitation if you like -7. Click **Invite** - -## Members pane removals: - -1. Click the **Room** or **Group** header -2. Select **Members** -3. Find and select any contact/s you’d like to remove -4. Click **Remove** -5. Click **Remove members** - -## How to format text - -- To italicize your message, place an underscore on both sides of the text: _text_ -- To bold your message, place an asterisk on both sides of the text: *text* -- To strikethrough your message, place a tilde on both sides of the text: ~text~ -- To turn your message into code, place a backtick on both sides of the text: `text` -- To turn your text into a blockquote, add an angled bracket (>) in front of the text: - >your text -- To turn your message into a heading, place a number sign (#) in front of the text: -# Heading -- To turn your entire message into code block, place three backticks on both sides of the text: -``` -here's some text -and even more text -``` - -## Message actions - -If you mouse-over a message (or long-press on mobile), you will see the action menu. This allows you to add a reaction, start a thread, copy the link, mark it as unread, edit your own message, delete your own message, or flag it as offensive. - -**Add a reaction**: React with an emoji to the message -**Start a thread**: Start a thread by responding to the message instead of replying in the parent room -**Copy the link**: Share the message link with people who have access (i.e. Group or Room members). Note: Anyone can access messages in Public rooms -**Mark is as unread**: This will highlight the message in your left hand menu -**Edit message**: You can edit your own messages anytime. When you edit a message it will show as *edited* -**Delete message**: Deleting a message will remove it entirely for all viewers -**Flag as offensive**: Flagging a message as offensive escalates it to Expensify’s internal moderation team for review. The person who sent the message will be notified anonymously of the flag and the moderation team will decide what further action is needed - -## Workspace chat rooms - -In addition to 1:1 and group chat, members of a Workspace will have access to two additional rooms; the #announce and #admins rooms. -All Workspace members are added to the #announce room by default. The #announce room lets you share important company announcements and have conversations between Workspace members. - -All Workspace admins can access the #admins room. Use the #admins room to collaborate with the other admins on your Workspace, and chat with your dedicated Expensify Setup Specialist. If you have a subscription of 10 or more members, you're automatically assigned an Account Manager. You can ask for help and collaborate with your Account Manager in this same #admins room. Anytime someone on your team, your dedicated Setup Specialist, or your dedicated account manager makes any changes to your Workspace settings, that update is logged in the #admins room. - -# Deep Dive - -## Flagging content as offensive - -In order to maintain a safe community for our members, Expensify provides tools to report offensive content and unwanted behavior in Expensify Chat. If you see a message (or attachment/image) from another member that you’d like our moderators to review, you can flag it by clicking the flag icon in the message context menu (on desktop) or holding down on the message and selecting “Flag as offensive” (on mobile). - -![Moderation Context Menu](https://help.expensify.com/assets/images/moderation-context-menu.png){:width="100%"} - -Once the flag is selected, you will be asked to categorize the message (such as spam, bullying, and harassment). Select what you feel best represents the issue is with the content, and you’re done - the message will be sent off to our internal team for review. - -![Moderation Flagging Options](https://help.expensify.com/assets/images/moderation-flag-page.png){:width="100%"} - -Depending on the severity of the offense, messages can be hidden (with an option to reveal) or fully removed, and in extreme cases, the sender of the message can be temporarily or permanently blocked from posting. - -You will receive a whisper from Concierge any time your content has been flagged, as well as when you have successfully flagged a piece of content. - -![Moderation Reportee Whisper](https://help.expensify.com/assets/images/moderation-reportee-whisper.png){:width="100%"} -![Moderation Reporter Whisper](https://help.expensify.com/assets/images/moderation-reporter-whisper.png){:width="100%"} - -*Note: Any message sent in public chat rooms are automatically reviewed by an automated system looking for offensive content and sent to our moderators for final decisions if it is found.* - -{% include faq-begin.md %} - -## What are the #announce and #admins rooms? - -In addition to 1:1, Groups, and Workspace rooms, members of a Workspace will have access to two additional rooms; the #announce and #admins rooms. - -All domain members are added to the #announce room by default. The #announce room lets you share important company announcements and have conversations between Workspace members. - -All Workspace admins can access the #admins room. Use the #admins room to collaborate with the other admins on your Workspace, and chat with your dedicated Expensify Setup Specialist. If you have an existing subscription, you're automatically assigned an Account Manager. You can ask for help and collaborate with your Account Manager in this room. - -## Someone I don’t recognize is in my #admins room for my Workspace; who is it? - -After creating your Workspace, you’ll have a dedicated Expensify Setup Sspecialist who will help you onboard and answer your questions. You can chat with them directly in the #admins room or request a call to talk to them over the phone. Later, once you've finished onboarding, if you have a subscription of 10 or more members, a dedicated Account Manager is added to your #admins room for ongoing product support. - -## Can I force a chat to stay at the top of the chats list? - -You sure can! Click on the chat you want to keep at the top of the list, and then click the small **pin** icon. If you want to unpin a chat, just click the **pin** icon again. - -## Can I change the way my chats are displayed? - -The way your chats display in the left-hand menu is customizable. We offer two different options; Most Recent mode and _#focus_ mode. - -- Most Recent mode will display all chats by default, sort them by the most recent, and keep your pinned chats at the top of the list. -- #focus mode will display only unread and pinned chats, and will sort them alphabetically. This setting is perfect for when you need to cut distractions and focus on a crucial project. - -You can find your display mode by clicking on your Profile > Preferences > Priority Mode. -{% include faq-end.md %} From 3efa0006e4ba5f4fe16225e905d422173950b82e Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:16:28 -0500 Subject: [PATCH 336/580] Create Edit-or-delete-messages.md New article --- .../chat/Edit-or-delete-messages.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/articles/new-expensify/chat/Edit-or-delete-messages.md diff --git a/docs/articles/new-expensify/chat/Edit-or-delete-messages.md b/docs/articles/new-expensify/chat/Edit-or-delete-messages.md new file mode 100644 index 000000000000..a19fee42e740 --- /dev/null +++ b/docs/articles/new-expensify/chat/Edit-or-delete-messages.md @@ -0,0 +1,31 @@ +--- +title: Edit or delete messages +description: Edit or delete chat messages you've sent +--- +<div id="new-expensify" markdown="1"> + +{% include info.html %} +You can edit or delete your *own* messages only. Deleting a message cannot be undone. +{% include end-info.html %} + +You have the option to edit or delete any of your messages: +- **Edit message**: Reopens a message so you can make changes. Once a message has been updated, an “edited” label will appear next to it. +- **Delete message**: Removes a message or image for all viewers. + +To edit or delete a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open a chat in your inbox. +2. Right-click a message and select **Edit comment** or **Delete comment**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open a chat in your inbox. +2. Press and hold a message and select **Edit comment** or **Delete comment**. +{% include end-option.html %} + +{% include end-selector.html %} + +</div> From c7a88d9cb66fe02c7bbe1bbaecf56c6474a5f5bc Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:21:49 -0500 Subject: [PATCH 337/580] Create Flag-chat-messages.md New article --- .../new-expensify/chat/Flag-chat-messages.md | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/articles/new-expensify/chat/Flag-chat-messages.md diff --git a/docs/articles/new-expensify/chat/Flag-chat-messages.md b/docs/articles/new-expensify/chat/Flag-chat-messages.md new file mode 100644 index 000000000000..4955298bbd6b --- /dev/null +++ b/docs/articles/new-expensify/chat/Flag-chat-messages.md @@ -0,0 +1,33 @@ +--- +title: Flag chat messages +description: Report a message as offensive, spam, etc. +--- +<div id="new-expensify" markdown="1"> + +Flagging a message as offensive (including unwanted behavior or offensive messages or attachments) escalates it to Expensify’s internal moderation team for review. The person who sent the message will be notified of the flag anonymously, and the moderation team will decide what further action is needed. + +Depending on the severity of the offense, messages can be hidden (with an option to reveal) or fully removed. In extreme cases, the sender of the message may be temporarily or permanently blocked from posting. + +{% include info.html %} +Messages sent in public chat rooms are automatically reviewed for offensive content by an automated system. If offensive content is found, the message is sent to Expensify’s internal moderation team for further review. +{% include end-info.html %} + +To flag a message, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat in your inbox. +2. Hover over the message and click the three dot menu icon that appears in the menu at the top right of the message. Then select **Flag as offensive**. +3. Select a category: spam, inconsiderate, intimidation, bullying, harassment, or assault. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat in your inbox. +2. Press and hold the message and select **Flag as offensive**. +3. Select a category: spam, inconsiderate, intimidation, bullying, harassment, or assault. +{% include end-option.html %} + +{% include end-selector.html %} + +</div> From cf26727ec5368c9080cee6ab46a6ab9e1560ecb5 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:28:11 -0500 Subject: [PATCH 338/580] Create Expensify-Chat-rooms-for-admins.md New article --- .../chat/Expensify-Chat-rooms-for-admins.md | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md diff --git a/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md b/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md new file mode 100644 index 000000000000..ff341d4de68f --- /dev/null +++ b/docs/articles/new-expensify/chat/Expensify-Chat-rooms-for-admins.md @@ -0,0 +1,50 @@ +--- +title: Expensify Chat rooms for admins +description: Use the announce and admins chat rooms +--- +<div id="new-expensify" markdown="1"> + +When a workspace is created, an #announce and #admins chat room is automatically created. + +# #announce + +All Workspace Members can use this room to share or discover important company announcements and have conversations with other members. + +By default, all Workspace Members are allowed to send messages in #announce rooms. However, Workspace Admins can update the permissions to allow only admins to post messages in the #announce room. + +## Update messaging permissions + +To allow only admins to post in an #announce room, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the #announce room chat in your inbox. +2. Click the room header. +3. Click **Settings**. +4. Click **Who can post** and select **Admins only**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the #announce room chat in your inbox. +2. Tap the room header. +3. Tap **Settings**. +4. Tap **Who can post** and select **Admins only**. +{% include end-option.html %} + +{% include end-selector.html %} + +# #admins + +Only Workspace Admins can access this room to collaborate with the other admins in the workspace. You can also use this space to: +- Chat with your dedicated Expensify Setup Specialist. +- Chat with your Account Manager (if you have a subscription with 10 or more members). +- Review changes made to your Workspace settings (includes changes made by someone on your team, your dedicated Expensify Setup Specialist, or your dedicated Account Manager). + +# FAQs + +**Someone I don’t recognize is in my #admins room for my Workspace.** + +Your #admins room also includes your dedicated Expensify Setup Specialist who will help you onboard and answer your questions. You can chat with them directly from your #admins room. If you have a subscription of 10 or more members, you can chat with your dedicated Account Manager, who is also added to your #admins room for ongoing product support. + +</div> From 5ea49d26760d4e802ed297410ce3f9b60e310029 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 24 Apr 2024 16:31:31 +0200 Subject: [PATCH 339/580] fix: remove wrappers, change the labels --- .../PersonalInfo/substeps/FullNameStep.tsx | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx index 2f0926dcdefb..8842cd65838b 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx @@ -59,28 +59,26 @@ function FullNameStep({onNext, isEditing}: SubStepProps) { > <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb6]}>{translate('personalInfoStep.whatsYourLegalName')}</Text> - <View style={[styles.mb6]}> - <InputWrapper - InputComponent={TextInput} - inputID={PERSONAL_INFO_STEP_KEY.FIRST_NAME} - label={translate('personalInfoStep.legalFirstName')} - aria-label={translate('personalInfoStep.legalFirstName')} - role={CONST.ROLE.PRESENTATION} - defaultValue={defaultValues.firstName} - shouldSaveDraft={!isEditing} - /> - </View> - <View style={[styles.mb6]}> - <InputWrapper - InputComponent={TextInput} - inputID={PERSONAL_INFO_STEP_KEY.LAST_NAME} - label={translate('personalInfoStep.legalLastName')} - aria-label={translate('personalInfoStep.legalLastName')} - role={CONST.ROLE.PRESENTATION} - defaultValue={defaultValues.lastName} - shouldSaveDraft={!isEditing} - /> - </View> + <InputWrapper + InputComponent={TextInput} + inputID={PERSONAL_INFO_STEP_KEY.FIRST_NAME} + label={translate('common.firstName')} + aria-label={translate('common.firstName')} + role={CONST.ROLE.PRESENTATION} + defaultValue={defaultValues.firstName} + shouldSaveDraft={!isEditing} + containerStyles={[styles.mb6]} + /> + <InputWrapper + InputComponent={TextInput} + inputID={PERSONAL_INFO_STEP_KEY.LAST_NAME} + label={translate('common.lastName')} + aria-label={translate('common.lastName')} + role={CONST.ROLE.PRESENTATION} + defaultValue={defaultValues.lastName} + shouldSaveDraft={!isEditing} + containerStyles={[styles.mb6]} + /> <HelpLinks /> </View> </FormProvider> From c423cb8b0aa248c9f792cf997d76d3ead84aa276 Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Wed, 24 Apr 2024 16:31:37 +0200 Subject: [PATCH 340/580] Add template of ExpenseListItem --- .../SelectionList/ExpenseListItem.tsx | 218 ++++++++++++++++++ src/components/SelectionList/types.ts | 6 +- src/pages/Search/SearchPage.tsx | 43 +++- 3 files changed, 257 insertions(+), 10 deletions(-) create mode 100644 src/components/SelectionList/ExpenseListItem.tsx diff --git a/src/components/SelectionList/ExpenseListItem.tsx b/src/components/SelectionList/ExpenseListItem.tsx new file mode 100644 index 000000000000..a460bd3b3d01 --- /dev/null +++ b/src/components/SelectionList/ExpenseListItem.tsx @@ -0,0 +1,218 @@ +import {format} from 'date-fns'; +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MultipleAvatars from '@components/MultipleAvatars'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import TextWithTooltip from '@components/TextWithTooltip'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import BaseListItem from './BaseListItem'; +import type {ExpenseListItemProps, ListItem} from './types'; + +function ExpenseListItem<TItem extends ListItem>({ + item, + isFocused, + showTooltip, + isDisabled, + canSelectMultiple, + onSelectRow, + onCheckboxPress, + onDismissError, + shouldPreventDefaultFocusOnSelectRow, + rightHandSideComponent, + onFocus, + shouldSyncFocus, +}: ExpenseListItemProps<TItem>) { + const styles = useThemeStyles(); + const theme = useTheme(); + const StyleUtils = useStyleUtils(); + const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; + const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; + + const handleCheckboxPress = useCallback(() => { + if (onCheckboxPress) { + onCheckboxPress(item); + } else { + onSelectRow(item); + } + }, [item, onCheckboxPress, onSelectRow]); + + return ( + <BaseListItem + item={item} + pressableStyle={[[styles.selectionListPressableItemWrapper, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive]]} + wrapperStyle={[styles.flexRow, styles.flex1, styles.justifyContentBetween, styles.userSelectNone, styles.alignItemsCenter]} + containerStyle={[styles.mb3]} + isFocused={isFocused} + isDisabled={isDisabled} + showTooltip={showTooltip} + canSelectMultiple={canSelectMultiple} + onSelectRow={onSelectRow} + onDismissError={onDismissError} + shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} + rightHandSideComponent={rightHandSideComponent} + errors={item.errors} + pendingAction={item.pendingAction} + keyForList={item.keyForList} + onFocus={onFocus} + shouldSyncFocus={shouldSyncFocus} + hoverStyle={item.isSelected && styles.activeComponentBG} + > + {(hovered) => ( + <> + {canSelectMultiple && ( + <PressableWithFeedback + accessibilityLabel={item.text ?? ''} + role={CONST.ROLE.BUTTON} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + disabled={isDisabled || item.isDisabledCheckbox} + onPress={handleCheckboxPress} + style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.mr3]} + > + <View style={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!item.isSelected, !!item.isDisabled)]}> + {item.isSelected && ( + <Icon + src={Expensicons.Checkmark} + fill={theme.textLight} + height={14} + width={14} + /> + )} + </View> + </PressableWithFeedback> + )} + {!!item.icons && ( + <MultipleAvatars + icons={item.icons ?? []} + shouldShowTooltip={showTooltip} + secondAvatarStyle={[ + StyleUtils.getBackgroundAndBorderStyle(theme.sidebar), + isFocused ? StyleUtils.getBackgroundAndBorderStyle(focusedBackgroundColor) : undefined, + hovered && !isFocused ? StyleUtils.getBackgroundAndBorderStyle(hoveredBackgroundColor) : undefined, + ]} + /> + )} + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={format(new Date(item.created), 'MMM dd')} + style={[ + styles.optionDisplayName, + isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, + styles.sidebarLinkText, + styles.pre, + styles.justifyContentCenter, + ]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.description} + style={[ + styles.optionDisplayName, + isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, + styles.sidebarLinkText, + styles.pre, + styles.justifyContentCenter, + ]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.from.displayName} + style={[ + styles.optionDisplayName, + isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, + styles.sidebarLinkText, + styles.pre, + + styles.justifyContentCenter, + ]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.to.displayName} + style={[ + styles.optionDisplayName, + isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, + styles.sidebarLinkText, + styles.pre, + + styles.justifyContentCenter, + ]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.category} + style={[ + styles.optionDisplayName, + styles.sidebarLinkActiveText, + styles.pre, + styles.justifyContentCenter, + ]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.tag} + style={[ + styles.optionDisplayName, + isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, + styles.sidebarLinkText, + styles.pre, + + styles.justifyContentCenter, + ]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsEnd]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.amount} + style={[ + styles.optionDisplayName, + isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, + styles.sidebarLinkText, + styles.pre, + + styles.justifyContentCenter, + ]} + /> + </View> + {/* + + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.amount} + style={[ + styles.optionDisplayName, + isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, + styles.sidebarLinkText, + styles.pre, + + styles.justifyContentCenter, + ]} + /> + </View> */} + {!!item.rightElement && item.rightElement} + </> + )} + </BaseListItem> + ); +} + +ExpenseListItem.displayName = 'ExpenseListItem'; + +export default ExpenseListItem; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 50929095dc91..f7c3697b2814 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -7,6 +7,7 @@ import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type IconAsset from '@src/types/utils/IconAsset'; +import type ExpenseListItem from './ExpenseListItem'; import type InviteMemberListItem from './InviteMemberListItem'; import type RadioListItem from './RadioListItem'; import type TableListItem from './TableListItem'; @@ -180,7 +181,9 @@ type RadioListItemProps<TItem extends ListItem> = ListItemProps<TItem>; type TableListItemProps<TItem extends ListItem> = ListItemProps<TItem>; -type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem; +type ExpenseListItemProps<TItem extends ListItem> = ListItemProps<TItem>; + +type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem | typeof ExpenseListItem; type Section<TItem extends ListItem> = { /** Title of the section */ @@ -373,6 +376,7 @@ export type { RadioListItemProps, TableListItemProps, InviteMemberListItemProps, + ExpenseListItemProps, ListItem, ListItemProps, FlattenedSectionsReturn, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index cdf92344d945..4dd33b25bb15 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,24 +1,47 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useState} from 'react'; -import {ValueOf} from 'type-fest'; +import React from 'react'; +import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import UserListItem from '@components/SelectionList/UserListItem'; +import ExpenseListItem from '@components/SelectionList/ExpenseListItem'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import Navigation from '@libs/Navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; -import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; -import SearchTableHeader from './SearchTableHeader'; import useCustomBackHandler from './useCustomBackHandler'; type SearchPageProps = StackScreenProps<CentralPaneNavigatorParamList, typeof SCREENS.SEARCH.CENTRAL_PANE>; function SearchPage({route}: SearchPageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); useCustomBackHandler(); + const getListHeader = () => { + // const showMerchantColumn = ReportUtils.shouldShowMerchantColumn(data); + const showMerchantColumn = isSmallScreenWidth && true; + + return ( + <View style={[styles.flex1, styles.flexRow, styles.justifyContentBetween, styles.pl3]}> + {/* <Text style={styles.searchInputStyle}>{translate('common.receipt')}</Text> */} + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.date')}</Text> + {showMerchantColumn && <Text style={[styles.searchInputStyle]}>{translate('common.merchant')}</Text>} + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.description')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.from')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.to')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.category')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.tag')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1, styles.textAlignRight]}>{translate('common.total')}</Text> + </View> + ); + }; + return ( <ScreenWrapper testID={SearchPage.displayName}> <HeaderWithBackButton @@ -28,12 +51,14 @@ function SearchPage({route}: SearchPageProps) { /> <SelectionList canSelectMultiple - headerContent={<SearchTableHeader />} - ListItem={UserListItem} + customListHeader={getListHeader()} + ListItem={ExpenseListItem} onSelectRow={() => {}} - sections={[]} + onSelectAll={() => {}} + sections={[{data: [], isDisabled: false}]} onCheckboxPress={() => {}} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> </ScreenWrapper> ); From 678985bad4db937959e8eab5ed8230aae0fc6444 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 09:43:44 -0500 Subject: [PATCH 341/580] Create Invite-members-to-a-chat-group-or-room.md New article --- .../Invite-members-to-a-chat-group-or-room.md | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md diff --git a/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md b/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md new file mode 100644 index 000000000000..d6877f71be07 --- /dev/null +++ b/docs/articles/new-expensify/chat/Invite-members-to-a-chat-group-or-room.md @@ -0,0 +1,97 @@ +--- +title: Invite members to a chat group or room +description: Add new people to a chat group or room +--- +<div id="new-expensify" markdown="1"> + +You can invite people to a group or room by: +- @mentioning them +- Using the Members pane of the chat +- Sharing a link or QR code + +{% include info.html %} +These options are available only for rooms and groups. You cannot add additional people to a private 1-on-1 chat between two people. +{% include end-info.html %} + +# Invite with mention + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. In the message field, type @ and the person’s name or email address. Repeat this step until all desired participants are listed. +3. Enter a message, if desired. +4. Press Enter on your keyboard or click the Send icon. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. In the message field, type @ and the person’s name or email address. Repeat this step until all desired participants are listed. +3. Enter a message, if desired. +4. Tap the Send icon. +{% include end-option.html %} + +{% include end-selector.html %} + +# Invite from the Members pane + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. Click the room or group header. +3. Click **Members**. +4. Click **Invite member**. +5. Find and select any contact(s) you’d like to invite. +6. Click **Invite**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. Tap the room or group header. +3. Tap **Members**. +4. Tap **Invite member**. +5. Find and select any contact(s) you’d like to invite. +6. Tap **Invite**. +{% include end-option.html %} + +{% include end-selector.html %} + +# Share chat link or QR code + +{% include info.html %} +If your group/room is Private, you can only share the chat link with other members of the group/room. If it is a public group/room, anyone can access the chat via the link. +{% include end-info.html %} + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Open the chat group or room you want to invite someone to. +2. Click the room or group header. +3. Click **Share code**. +4. Copy the link or share the QR code. + - **Copy link**: Click **Copy URL** and paste the link into another chat, email, Slack, etc. + - **Share QR Code**: Present your device to another user so they can scan the QR code with their device. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Open the chat group or room you want to invite someone to. +2. Tap the room or group header. +3. Tap **Share code**. +4. Copy the link or share the QR code. + - **Copy link**: Tap **Copy URL** and paste the link into another chat, email, Slack, etc. + - **Share QR Code**: Present your device to another user so they can scan the QR code with their device. +{% include end-option.html %} + +{% include end-selector.html %} + +# FAQs + +**How do I remove someone from a chat group or room?** + +Currently, members have to remove themselves from a chat. + +</div> + + + From 4b5db30ee86f443b66da0a9151ac7a661046d9e5 Mon Sep 17 00:00:00 2001 From: Artem Makushov <waterim3009@gmail.com> Date: Wed, 24 Apr 2024 16:52:22 +0200 Subject: [PATCH 342/580] fix translations, CONST --- src/CONST.ts | 1 - src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/IOU.ts | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index c477cb735c0f..78ef4e1873c4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1454,7 +1454,6 @@ const CONST = { PAY: 'pay', SPLIT: 'split', REQUEST: 'request', - TRACK_EXPENSE: 'track-expense', INVOICE: 'invoice', SUBMIT: 'submit', TRACK: 'track', diff --git a/src/languages/en.ts b/src/languages/en.ts index 824e1eca5722..b1c3a45e9db7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -518,6 +518,7 @@ export default { split: 'split an expense', submit: 'submit an expense', track: 'track an expense', + invoice: 'invoice an expense', }, }, reportAction: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 529084aa2061..53c631036ab5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -511,6 +511,7 @@ export default { split: 'dividir un gasto', submit: 'presentar un gasto', track: 'rastrear un gasto', + invoice: 'facturar un gasto', }, }, reportAction: { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 2e5a63330455..45bcf7c2fad1 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5337,7 +5337,7 @@ function canIOUBePaid(iouReport: OnyxEntry<OnyxTypes.Report> | EmptyObject, chat return chatReport?.invoiceReceiver?.accountID === userAccountID; } - return getPolicy(chatReport?.invoiceReceiver?.policyID).role === CONST.POLICY.ROLE.ADMIN; + return PolicyUtils.getPolicy(chatReport?.invoiceReceiver?.policyID).role === CONST.POLICY.ROLE.ADMIN; } const isPayer = ReportUtils.isPayer( From f989cea2a366de65c33dd7b6449997f241c8fb28 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Wed, 24 Apr 2024 17:43:41 +0200 Subject: [PATCH 343/580] fix PR comments --- src/ROUTES.ts | 16 ++++++++-------- .../settings/Wallet/ActivatePhysicalCardPage.tsx | 12 +++++------- src/pages/settings/Wallet/ExpensifyCardPage.tsx | 12 +++++------- src/pages/settings/Wallet/PaymentMethodList.tsx | 3 +-- src/pages/settings/Wallet/ReportCardLostPage.tsx | 2 +- .../Wallet/ReportVirtualCardFraudPage.tsx | 4 ++-- 6 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fef295d64b2b..87180f7d01cb 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -92,12 +92,12 @@ const ROUTES = { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { - route: 'settings/wallet/card/:domain/:cardID?', - getRoute: (domain: string, cardID?: string) => (cardID ? (`settings/wallet/card/${domain}/${cardID}` as const) : (`settings/wallet/card/${domain}` as const)), + route: 'settings/wallet/card/:cardID?', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}` as const, }, SETTINGS_REPORT_FRAUD: { - route: 'settings/wallet/card/:domain/:cardID/report-virtual-fraud', - getRoute: (domain: string, cardID: string) => `settings/wallet/card/${domain}/${cardID}/report-virtual-fraud` as const, + route: 'settings/wallet/card/:cardID/report-virtual-fraud', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-virtual-fraud` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: { route: 'settings/wallet/card/:domain/get-physical/name', @@ -126,12 +126,12 @@ const ROUTES = { SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { - route: 'settings/wallet/card/:domain/:cardID/report-card-lost-or-damaged', - getRoute: (domain: string, cardID: string) => `settings/wallet/card/${domain}/${cardID}/report-card-lost-or-damaged` as const, + route: 'settings/wallet/card/:cardID/report-card-lost-or-damaged', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/report-card-lost-or-damaged` as const, }, SETTINGS_WALLET_CARD_ACTIVATE: { - route: 'settings/wallet/card/:domain/:cardID/activate', - getRoute: (domain: string, cardID: string) => `settings/wallet/card/${domain}/${cardID}/activate` as const, + route: 'settings/wallet/card/:cardID/activate', + getRoute: (cardID: string) => `settings/wallet/card/${cardID}/activate` as const, }, SETTINGS_LEGAL_NAME: 'settings/profile/legal-name', SETTINGS_DATE_OF_BIRTH: 'settings/profile/date-of-birth', diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx index ceacb2797724..1d1d6583ffa8 100644 --- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx +++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.tsx @@ -15,7 +15,6 @@ import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as CardUtils from '@libs/CardUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -42,7 +41,7 @@ const MAGIC_INPUT_MIN_HEIGHT = 86; function ActivatePhysicalCardPage({ cardList, route: { - params: {domain = '', cardID = ''}, + params: {cardID = ''}, }, }: ActivatePhysicalCardPageProps) { const theme = useTheme(); @@ -55,8 +54,7 @@ function ActivatePhysicalCardPage({ const [lastFourDigits, setLastFourDigits] = useState(''); const [lastPressedDigit, setLastPressedDigit] = useState(''); - const domainCards = CardUtils.getDomainCards(cardList)[domain] ?? []; - const inactiveCard = domainCards.find((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED); + const inactiveCard = cardList?.[cardID]; const cardError = ErrorUtils.getLatestErrorMessage(inactiveCard ?? {}); const activateCardCodeInputRef = useRef<MagicCodeInputHandle>(null); @@ -69,8 +67,8 @@ function ActivatePhysicalCardPage({ return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID)); - }, [cardID, cardList, domain, inactiveCard?.isLoading, inactiveCard?.state]); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID)); + }, [cardID, cardList, inactiveCard?.isLoading, inactiveCard?.state]); useEffect( () => () => { @@ -124,7 +122,7 @@ function ActivatePhysicalCardPage({ return ( <IllustratedHeaderPageLayout title={translate('activateCardPage.activateCard')} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))} backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.PREFERENCES.ROOT].backgroundColor} illustration={LottieAnimations.Magician} scrollViewContainerStyles={[styles.mnh100]} diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index 901d66386701..fa13524310c2 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -78,13 +78,14 @@ function ExpensifyCardPage({ privatePersonalDetails, loginList, route: { - params: {domain = '', cardID = ''}, + params: {cardID = ''}, }, }: ExpensifyCardPageProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); const shouldDisplayCardDomain = !cardList?.[cardID]?.nameValuePairs?.issuedBy; + const domain = cardList?.[cardID].domainName ?? ''; const pageTitle = shouldDisplayCardDomain ? translate('cardPage.expensifyCard') : cardList?.[cardID]?.nameValuePairs?.cardTitle ?? translate('cardPage.expensifyCard'); const [isNotFound, setIsNotFound] = useState(false); @@ -249,7 +250,7 @@ function ExpensifyCardPage({ titleStyle={styles.walletCardMenuItem} icon={Expensicons.Flag} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain, String(card.cardID)))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(String(card.cardID)))} /> </> ))} @@ -269,7 +270,7 @@ function ExpensifyCardPage({ title={translate('reportCardLostOrDamaged.report')} icon={Expensicons.Flag} shouldShowRightIcon - onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(domain, String(card.cardID)))} + onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED.getRoute(String(card.cardID)))} /> </> ); @@ -284,10 +285,7 @@ function ExpensifyCardPage({ style={[styles.w100, styles.p5]} onPress={() => Navigation.navigate( - ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute( - domain, - String(physicalCards?.find((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED)?.cardID), - ), + ROUTES.SETTINGS_WALLET_CARD_ACTIVATE.getRoute(String(physicalCards?.find((card) => card?.state === CONST.EXPENSIFY_CARD.STATE.NOT_ACTIVATED)?.cardID)), ) } text={translate('activateCardPage.activatePhysicalCard')} diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 47da6ad19530..f684b76a152e 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -244,8 +244,7 @@ function PaymentMethodList({ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing title: isAdminIssuedVirtualCard ? card?.nameValuePairs?.cardTitle || card.bank : card.bank, description: card.domainName, - onPress: () => - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName ?? '', isAdminIssuedVirtualCard ? card.cardID.toString() ?? '' : undefined)), + onPress: () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(String(card.cardID))), isGroupedCardDomain: !isAdminIssuedVirtualCard, shouldShowRightIcon: true, interactive: true, diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index d914060cdd31..02b873f95a4e 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -99,7 +99,7 @@ function ReportCardLostPage({ return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID)); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID)); }, [domain, formData?.isLoading, prevIsLoading, physicalCard?.errors, cardID]); useEffect(() => { diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx index 22f8062183b5..e98e70e74f13 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx @@ -55,7 +55,7 @@ function ReportVirtualCardFraudPage({ return; } - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID)); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID)); }, [cardID, domain, formData?.isLoading, prevIsLoading, virtualCard?.errors]); if (isEmptyObject(virtualCard)) { @@ -66,7 +66,7 @@ function ReportVirtualCardFraudPage({ <ScreenWrapper testID={ReportVirtualCardFraudPage.displayName}> <HeaderWithBackButton title={translate('reportFraudPage.title')} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))} /> <View style={[styles.flex1, styles.justifyContentBetween]}> <Text style={[styles.webViewStyles.baseFontStyle, styles.mh5]}>{translate('reportFraudPage.description')}</Text> From 81f96b7bd7970a53c5a2d7d8cff60ea36714f1c2 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Wed, 24 Apr 2024 11:07:15 -0500 Subject: [PATCH 344/580] Create Send-an-invoice.md New article --- .../new-expensify/expenses/Send-an-invoice.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/articles/new-expensify/expenses/Send-an-invoice.md diff --git a/docs/articles/new-expensify/expenses/Send-an-invoice.md b/docs/articles/new-expensify/expenses/Send-an-invoice.md new file mode 100644 index 000000000000..588f0da20154 --- /dev/null +++ b/docs/articles/new-expensify/expenses/Send-an-invoice.md @@ -0,0 +1,52 @@ +--- +title: Send an invoice +description: Notify a customer that a payment is due +--- +<div id="new-expensify" markdown="1"> + +You can send invoices directly from Expensify to notify customers that a payment is due. + +To create and send an invoice, + +{% include selector.html values="desktop, mobile" %} + +{% include option.html value="desktop" %} +1. Click the + icon in the bottom left menu and select **Send Invoice**. +2. Enter the amount due and click **Next**. +3. Enter the email or phone number of the person who should receive the invoice. +4. (Optional) Add additional invoice details, including a description, date, category, tag, and/or tax. +5. Click **Send**. +{% include end-option.html %} + +{% include option.html value="mobile" %} +1. Tap the + icon in the bottom left menu and select **Send Invoice**. +2. Enter the amount due and tap **Next**. +3. Enter the email or phone number of the person who should receive the invoice. +4. (Optional) Add additional invoice details, including a description, date, category, tag, and/or tax. +5. Tap **Send**. +{% include end-option.html %} + +{% include end-selector.html %} + +# How the customer pays an invoice + +Once an invoice is sent, the customer receives an automated email or text message to notify them of the invoice. They can use this notification to pay the invoice whenever they are ready. They will: + +1. Click the link in the email or text notification they receive from Expensify. +2. Click **Pay**. +3. Choose **Paying as an individual** or **Paying as a business**. +4. Click **Pay Elsewhere**, which will mark the invoice as Paid. + +Currently, invoices must be paid outside of Expensify. However, the ability to make payments through Expensify is coming soon. + +# FAQs + +**How do I communicate with the sender/recipient about the invoice?** + +You can communicate with the recipient in New Expensify. After sending an invoice, Expensify automatically creates an invoice room between the invoice sender and the payer to discuss anything related to the invoice. You can invite users to join the conversation, remove them from the room, and leave the room at any time. + +**Can you import and export invoices between an accounting integration?** + +Yes, you can export and import invoices between Expensify and your QuickBooks Online or Xero integration. + +</div> From f11489447cdec52b4f30ad4fcf6f498879bdc52f Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Wed, 24 Apr 2024 18:45:13 +0200 Subject: [PATCH 345/580] fix navigation bugs --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 6 +++--- src/pages/settings/Wallet/ReportCardLostPage.tsx | 4 ++-- src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 1889fc556d0b..fb98fe23192d 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -127,7 +127,7 @@ function BaseGetPhysicalCard({ // When there's no physical card or it exists but it doesn't have the required state for this flow, // redirect user to the espensify card page if (cardToBeIssued.state !== CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED) { - Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardToBeIssued.cardID.toString())); + Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardToBeIssued.cardID.toString())); return; } @@ -152,7 +152,7 @@ function BaseGetPhysicalCard({ // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); - Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID.toString())); + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString())); return; } GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); @@ -165,7 +165,7 @@ function BaseGetPhysicalCard({ > <HeaderWithBackButton title={title} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain, cardID.toString()))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString()))} /> <Text style={[styles.textHeadline, styles.mh5, styles.mb5]}>{headline}</Text> {renderContent({onSubmit, submitButtonText, children, onValidate})} diff --git a/src/pages/settings/Wallet/ReportCardLostPage.tsx b/src/pages/settings/Wallet/ReportCardLostPage.tsx index 02b873f95a4e..9d6c69249130 100644 --- a/src/pages/settings/Wallet/ReportCardLostPage.tsx +++ b/src/pages/settings/Wallet/ReportCardLostPage.tsx @@ -75,7 +75,7 @@ function ReportCardLostPage({ }, cardList = {}, route: { - params: {domain = '', cardID = ''}, + params: {cardID = ''}, }, formData, }: ReportCardLostPageProps) { @@ -100,7 +100,7 @@ function ReportCardLostPage({ } Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID)); - }, [domain, formData?.isLoading, prevIsLoading, physicalCard?.errors, cardID]); + }, [formData?.isLoading, prevIsLoading, physicalCard?.errors, cardID]); useEffect(() => { if (formData?.isLoading && isEmptyObject(physicalCard?.errors)) { diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx index e98e70e74f13..76f5f4c1bef3 100644 --- a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.tsx @@ -34,7 +34,7 @@ type ReportVirtualCardFraudPageProps = ReportVirtualCardFraudPageOnyxProps & Sta function ReportVirtualCardFraudPage({ route: { - params: {domain = '', cardID = ''}, + params: {cardID = ''}, }, cardList, formData, @@ -56,7 +56,7 @@ function ReportVirtualCardFraudPage({ } Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID)); - }, [cardID, domain, formData?.isLoading, prevIsLoading, virtualCard?.errors]); + }, [cardID, formData?.isLoading, prevIsLoading, virtualCard?.errors]); if (isEmptyObject(virtualCard)) { return <NotFoundPage />; From 57f1a7b2cf91e62457f0897a8994c2d1269c4dc9 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Wed, 24 Apr 2024 19:04:41 +0200 Subject: [PATCH 346/580] fix: address step title --- src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx index 7d62eff8072e..a6a950d22adb 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx @@ -68,7 +68,7 @@ function AddressStep({onNext, isEditing}: SubStepProps) { style={[styles.mh5, styles.flexGrow1]} > <View> - <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.enterYourAddress')}</Text> + <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.whatsYourAddress')}</Text> <Text style={[styles.textSupporting]}>{translate('common.noPO')}</Text> <AddressFormFields inputKeys={INPUT_KEYS} From d54260082d0eb04f79b064e020f4f98b71b9adab Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Wed, 24 Apr 2024 19:05:03 +0200 Subject: [PATCH 347/580] remove redundant types --- src/libs/Navigation/types.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 039868f9de65..a57816e692f5 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -119,20 +119,14 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.WALLET.ROOT]: undefined; [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: undefined; [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: { - /** domain of selected card */ - domain: string; /** cardID of selected card */ cardID: string; }; [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: { - /** domain of selected card */ - domain: string; /** cardID of selected card */ cardID: string; }; [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: { - /** domain of selected card */ - domain: string; /** cardID of selected card */ cardID: string; }; @@ -309,8 +303,6 @@ type SettingsNavigatorParamList = { }; [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: BackToParams; [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: { - /** domain of selected card */ - domain: string; /** cardID of selected card */ cardID: string; }; From de1c1069585aa913afb8255bf9b4eed36fafa8d0 Mon Sep 17 00:00:00 2001 From: Sheena Trepanier <sheena@expensify.com> Date: Wed, 24 Apr 2024 10:33:19 -0700 Subject: [PATCH 348/580] Rename unlimited-virtual-cards.md to Unlimited-Virtual-Cards.md Updated the name per @rushatgabhane's suggestion. --- .../{unlimited-virtual-cards.md => Unlimited-Virtual-Cards.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/expensify-card/{unlimited-virtual-cards.md => Unlimited-Virtual-Cards.md} (100%) diff --git a/docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md b/docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md similarity index 100% rename from docs/articles/expensify-classic/expensify-card/unlimited-virtual-cards.md rename to docs/articles/expensify-classic/expensify-card/Unlimited-Virtual-Cards.md From 4102489ffff306fb396e3dff8dc021602caa068c Mon Sep 17 00:00:00 2001 From: gijoe0295 <gijoe0295@gmail.com> Date: Thu, 25 Apr 2024 00:34:13 +0700 Subject: [PATCH 349/580] fix: 404 when approving an unheld amount --- src/components/ProcessMoneyReportHoldMenu.tsx | 6 ++++++ src/libs/ReportActionsUtils.ts | 9 +++++++++ src/libs/TransactionUtils.ts | 7 ++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 5de433960092..6e81c9d57bc8 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -1,7 +1,10 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import {isLinkedTransactionHeld} from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; +import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import DecisionModal from './DecisionModal'; @@ -52,6 +55,9 @@ function ProcessMoneyReportHoldMenu({ const onSubmit = (full: boolean) => { if (isApprove) { IOU.approveMoneyRequest(moneyRequestReport, full); + if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '', moneyRequestReport.reportID)) { + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport.reportID)); + } } else if (chatReport && paymentType) { IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport, full); } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8a63a031f4e5..44d03c2e5aac 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -28,6 +28,7 @@ import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import type {OptimisticIOUReportAction} from './ReportUtils'; +import * as TransactionUtils from './TransactionUtils'; type LastVisibleMessage = { lastMessageTranslationKey?: string; @@ -1107,6 +1108,13 @@ function getReportActionMessageText(reportAction: OnyxEntry<ReportAction> | Empt return reportAction?.message?.reduce((acc, curr) => `${acc}${curr?.text}`, '') ?? ''; } +/** + * Check if the linked transaction is on hold + */ +function isLinkedTransactionHeld(reportActionID: string, reportID: string): boolean { + return TransactionUtils.isOnHold(getLinkedTransactionID(reportActionID, reportID)); +} + export { extractLinksFromMessageHtml, getOneTransactionThreadReportID, @@ -1168,6 +1176,7 @@ export { isActionableJoinRequest, isActionableJoinRequestPending, isActionableTrackExpense, + isLinkedTransactionHeld, }; export type {LastVisibleMessage}; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index eb794d2199f0..723c1225df22 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -578,12 +578,13 @@ function getRecentTransactions(transactions: Record<string, string>, size = 2): /** * Check if transaction is on hold */ -function isOnHold(transaction: OnyxEntry<Transaction>): boolean { - if (!transaction) { +function isOnHold(transactionOrID: OnyxEntry<Transaction> | string): boolean { + if (!transactionOrID) { return false; } - return !!transaction.comment?.hold; + const transaction = typeof transactionOrID === 'string' ? allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionOrID}`] : transactionOrID; + return !!transaction?.comment?.hold; } /** From f3c7b3c20d4c7dfdf21e601ce4a339f3f68d5b27 Mon Sep 17 00:00:00 2001 From: gijoe0295 <gijoe0295@gmail.com> Date: Thu, 25 Apr 2024 01:22:08 +0700 Subject: [PATCH 350/580] use transactionID --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/TransactionUtils.ts | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 44d03c2e5aac..56fca4bb7c0b 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1112,7 +1112,7 @@ function getReportActionMessageText(reportAction: OnyxEntry<ReportAction> | Empt * Check if the linked transaction is on hold */ function isLinkedTransactionHeld(reportActionID: string, reportID: string): boolean { - return TransactionUtils.isOnHold(getLinkedTransactionID(reportActionID, reportID)); + return TransactionUtils.isOnHoldByTransactionID(getLinkedTransactionID(reportActionID, reportID) ?? ''); } export { diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 723c1225df22..54f535c00744 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -578,13 +578,23 @@ function getRecentTransactions(transactions: Record<string, string>, size = 2): /** * Check if transaction is on hold */ -function isOnHold(transactionOrID: OnyxEntry<Transaction> | string): boolean { - if (!transactionOrID) { +function isOnHold(transaction: OnyxEntry<Transaction>): boolean { + if (!transaction) { return false; } - const transaction = typeof transactionOrID === 'string' ? allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionOrID}`] : transactionOrID; - return !!transaction?.comment?.hold; + return !!transaction.comment?.hold; +} + +/** + * Check if transaction is on hold + */ +function isOnHoldByTransactionID(transactionID: string): boolean { + if (!transactionID) { + return false; + } + + return isOnHold(allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null); } /** @@ -689,6 +699,7 @@ export { isPending, isPosted, isOnHold, + isOnHoldByTransactionID, getWaypoints, isAmountMissing, isMerchantMissing, From 1c61fc0a61d52356f70eb96033dbae6625f39091 Mon Sep 17 00:00:00 2001 From: gijoe0295 <gijoe0295@gmail.com> Date: Thu, 25 Apr 2024 01:25:33 +0700 Subject: [PATCH 351/580] modify comment --- 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 54f535c00744..2508d45dfc78 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -587,7 +587,7 @@ function isOnHold(transaction: OnyxEntry<Transaction>): boolean { } /** - * Check if transaction is on hold + * Check if transaction is on hold by transactionID */ function isOnHoldByTransactionID(transactionID: string): boolean { if (!transactionID) { From 60160b6762518161bf46dbb2df1ced6151628185 Mon Sep 17 00:00:00 2001 From: gijoe0295 <gijoe0295@gmail.com> Date: Thu, 25 Apr 2024 01:27:04 +0700 Subject: [PATCH 352/580] modify comment --- 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 2508d45dfc78..73efe4083623 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -587,7 +587,7 @@ function isOnHold(transaction: OnyxEntry<Transaction>): boolean { } /** - * Check if transaction is on hold by transactionID + * Check if transaction is on hold for the given transactionID */ function isOnHoldByTransactionID(transactionID: string): boolean { if (!transactionID) { From db79f8ea1a1a6debe04514c2ae117aa2cbe4bb60 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 01:48:56 +0700 Subject: [PATCH 353/580] fix lint --- src/components/Avatar.tsx | 2 +- src/components/MultipleAvatars.tsx | 2 -- src/pages/home/report/ReportActionItemSingle.tsx | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 5cccb0408f7d..ba36a172fb02 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -52,7 +52,7 @@ type AvatarProps = { name?: string; /** Optional account id if it's user avatar or policy id if it's workspace avatar */ - accountID?: number; + accountID?: number | string; }; function Avatar({ diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 9916eea7fe22..d5fcf6607179 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -159,7 +159,6 @@ function MultipleAvatars({ accountID={icons[0].id} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} - accountID={icons[0].id} /> </View> </UserDetailsTooltip> @@ -210,7 +209,6 @@ function MultipleAvatars({ accountID={icon.id} type={icon.type} fallbackIcon={icon.fallbackIcon} - accountID={icon.id} /> </View> </UserDetailsTooltip> diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 6293c1e69a94..707e08ef88d8 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -206,7 +206,6 @@ function ReportActionItemSingle({ name={icon.name} accountID={icon.id} fallbackIcon={fallbackIcon} - accountID={icon.id} /> </View> </UserDetailsTooltip> From 0ea1fb2a7d091fbe300a37606fef7569808b1c06 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 01:53:20 +0700 Subject: [PATCH 354/580] fix typescript check --- src/types/onyx/OnyxCommon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index de2a889d27d7..c4a3afc3e0b9 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -34,7 +34,7 @@ type Icon = { name?: string; /** Avatar id */ - id?: number; + id?: number | string; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon?: AvatarSource; From 59485006aaa88ca6e8b6b4e351fb31cad16f796c Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 01:59:29 +0700 Subject: [PATCH 355/580] fix ts check --- src/components/Avatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index ba36a172fb02..003196e873e2 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -86,7 +86,7 @@ function Avatar({ const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; // We pass the color styles down to the SVG for the workspace and fallback avatar. - const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, accountID); + const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, Number(accountID)); const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; From a1db3e049f912076f19d37127651658d3774d882 Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Wed, 24 Apr 2024 21:24:44 +0100 Subject: [PATCH 356/580] revert style --- src/styles/index.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index c4195c256957..e3f5b4dc5500 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -398,14 +398,6 @@ const styles = (theme: ThemeColors) => lineHeight: variables.lineHeightSmall, }, - textMicroBoldColor: { - color: theme.text, - }, - - textMicroBoldDangerColor: { - color: theme.textLight, - }, - textMicroSupporting: { color: theme.textSupporting, fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, @@ -4323,14 +4315,6 @@ const styles = (theme: ThemeColors) => marginRight: 12, }, - moneyRequestHeaderStatusBarBadgeBackground: { - backgroundColor: theme.border, - }, - - moneyRequestHeaderStatusBarBadgeDangerBackground: { - backgroundColor: theme.danger, - }, - staticHeaderImage: { minHeight: 240, }, From 46967555a7c8f3bde34228380c70e1d1d1a20328 Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Wed, 24 Apr 2024 21:25:32 +0100 Subject: [PATCH 357/580] undo style --- src/styles/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/index.ts b/src/styles/index.ts index e3f5b4dc5500..25326fdd5d20 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -544,6 +544,7 @@ const styles = (theme: ThemeColors) => display: 'flex', justifyContent: 'center', alignItems: 'center', + backgroundColor: theme.border, }, button: { From 491ee38b0ad079ee53fa651fb6a69549e1940895 Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Wed, 24 Apr 2024 21:29:42 +0100 Subject: [PATCH 358/580] undo style --- src/styles/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 25326fdd5d20..7b79099532fb 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -544,7 +544,6 @@ const styles = (theme: ThemeColors) => display: 'flex', justifyContent: 'center', alignItems: 'center', - backgroundColor: theme.border, }, button: { @@ -4313,6 +4312,7 @@ const styles = (theme: ThemeColors) => display: 'flex', justifyContent: 'center', alignItems: 'center', + backgroundColor: theme.border, marginRight: 12, }, From 2e551c7613f64717aeb00ccf50fa77288c8c1f0a Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Wed, 24 Apr 2024 21:31:57 +0100 Subject: [PATCH 359/580] undo MoneyRequestHeaderStatusBar --- src/components/MoneyRequestHeaderStatusBar.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index 2ca8195c6d3f..59ef4ee0bd26 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -12,21 +12,15 @@ type MoneyRequestHeaderStatusBarProps = { /** Whether we show the border bottom */ shouldShowBorderBottom: boolean; - - /** Red Badge background */ - danger?: boolean; }; -function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom, danger}: MoneyRequestHeaderStatusBarProps) { +function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom}: MoneyRequestHeaderStatusBarProps) { const styles = useThemeStyles(); const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; - const badgeBackgroundColorStyle = danger ? styles.moneyRequestHeaderStatusBarBadgeDangerBackground : styles.moneyRequestHeaderStatusBarBadgeBackground; - const badgeTextColorStyle = danger ? styles.textMicroBoldDangerColor : styles.textMicroBoldColor; - return ( <View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.flexGrow1, styles.overflowHidden, styles.ph5, styles.pb3, borderBottomStyle]}> - <View style={[styles.moneyRequestHeaderStatusBarBadge, badgeBackgroundColorStyle]}> - <Text style={[styles.textStrong, styles.textMicroBold, badgeTextColorStyle]}>{title}</Text> + <View style={[styles.moneyRequestHeaderStatusBarBadge]}> + <Text style={[styles.textStrong, styles.textMicroBold]}>{title}</Text> </View> <View style={[styles.flexShrink1]}> <Text style={[styles.textLabelSupporting]}>{description}</Text> From b8ff77e002fe187aa9fbff2ca68fa0907f5e0e51 Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Wed, 24 Apr 2024 21:35:27 +0100 Subject: [PATCH 360/580] undo MoneyReportHeaderStatusBar --- src/components/MoneyReportHeaderStatusBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx index af23a63b21d0..7d2b749cce0a 100644 --- a/src/components/MoneyReportHeaderStatusBar.tsx +++ b/src/components/MoneyReportHeaderStatusBar.tsx @@ -24,7 +24,7 @@ function MoneyReportHeaderStatusBar({nextStep}: MoneyReportHeaderStatusBarProps) return ( <View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.overflowHidden, styles.w100]}> - <View style={[styles.moneyRequestHeaderStatusBarBadge, styles.moneyRequestHeaderStatusBarBadgeBackground]}> + <View style={styles.moneyRequestHeaderStatusBarBadge}> <Text style={[styles.textLabel, styles.textMicroBold]}>{translate(nextStep.title === CONST.NEXT_STEP.FINISHED ? 'iou.finished' : 'iou.nextStep')}</Text> </View> <View style={[styles.dFlex, styles.flexRow, styles.flexShrink1]}> From 9633c50532b23f73effb79e3bd73bade3a6f97ff Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 17:03:23 -0400 Subject: [PATCH 361/580] feat: add new UI text --- src/languages/en.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 728b214d65cf..65bfaaa4065d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1953,6 +1953,7 @@ export default { deleteFailureMessage: 'An error occurred while deleting the category, please try again.', categoryName: 'Category name', requiresCategory: 'Members must categorize all spend', + needCategoryForExportToIntegration: 'A category is required on every expense in order to export to', subtitle: 'Get a better overview of where money is being spent. Use our default categories or add your own.', emptyCategories: { title: "You haven't created any categories", From a8949461763278351f058552578b10e8a5711c8d Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 17:03:58 -0400 Subject: [PATCH 362/580] feat: add spanish translation --- src/languages/es.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index 6ca7c4a210e1..32c9d967bd40 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1980,6 +1980,7 @@ export default { deleteFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.', categoryName: 'Nombre de la categoría', requiresCategory: 'Los miembros deben categorizar todos los gastos', + needCategoryForExportToIntegration: 'Se requiere una categoría en cada gasto para poder exportarlo a', subtitle: 'Obtén una visión general de dónde te gastas el dinero. Utiliza las categorías predeterminadas o añade las tuyas propias.', emptyCategories: { title: 'No has creado ninguna categoría', From e8ce24074d981fc527ec9c631ea4a66e1eda0698 Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Wed, 24 Apr 2024 22:20:52 +0100 Subject: [PATCH 363/580] update with new badge --- src/components/MoneyRequestHeader.tsx | 4 ++-- src/components/MoneyRequestHeaderStatusBar.tsx | 14 +++++++++++--- src/pages/ProcessMoneyRequestHoldPage.tsx | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index e79e09f1a496..429d3a36e734 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -199,9 +199,9 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, {isOnHold && ( <MoneyRequestHeaderStatusBar title={translate('iou.hold')} - description={translate('iou.requestOnHold')} + description={translate('iou.expenseOnHold')} shouldShowBorderBottom - danger + error /> )} </View> diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index 59ef4ee0bd26..cc0b1279204e 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import Badge from './Badge'; import Text from './Text'; type MoneyRequestHeaderStatusBarProps = { @@ -12,15 +13,22 @@ type MoneyRequestHeaderStatusBarProps = { /** Whether we show the border bottom */ shouldShowBorderBottom: boolean; + + /** Is Error type */ + error?: boolean; }; -function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom}: MoneyRequestHeaderStatusBarProps) { +function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom, error = false}: MoneyRequestHeaderStatusBarProps) { const styles = useThemeStyles(); const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; return ( <View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.flexGrow1, styles.overflowHidden, styles.ph5, styles.pb3, borderBottomStyle]}> - <View style={[styles.moneyRequestHeaderStatusBarBadge]}> - <Text style={[styles.textStrong, styles.textMicroBold]}>{title}</Text> + <View style={[styles.mr3]}> + <Badge + text={title} + badgeStyles={styles.ml0} + error={error} + /> </View> <View style={[styles.flexShrink1]}> <Text style={[styles.textLabelSupporting]}>{description}</Text> diff --git a/src/pages/ProcessMoneyRequestHoldPage.tsx b/src/pages/ProcessMoneyRequestHoldPage.tsx index ec305cc5adfb..ad04ada569bb 100644 --- a/src/pages/ProcessMoneyRequestHoldPage.tsx +++ b/src/pages/ProcessMoneyRequestHoldPage.tsx @@ -33,7 +33,7 @@ function ProcessMoneyRequestHoldPage() { return ( <HeaderPageLayout - title={translate('common.back')} + title={translate('iou.hold')} footer={footerComponent} onBackButtonPress={() => Navigation.goBack()} testID={ProcessMoneyRequestHoldPage.displayName} From 88c3d4c0e9540f40c69174cc208747a76ab29361 Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Wed, 24 Apr 2024 22:29:01 +0100 Subject: [PATCH 364/580] update MoneyReportHeaderStatusBar too --- src/components/MoneyReportHeaderStatusBar.tsx | 9 ++++++--- src/styles/index.ts | 11 ----------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx index 7d2b749cce0a..82a82e014697 100644 --- a/src/components/MoneyReportHeaderStatusBar.tsx +++ b/src/components/MoneyReportHeaderStatusBar.tsx @@ -5,8 +5,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as NextStepUtils from '@libs/NextStepUtils'; import CONST from '@src/CONST'; import type ReportNextStep from '@src/types/onyx/ReportNextStep'; +import Badge from './Badge'; import RenderHTML from './RenderHTML'; -import Text from './Text'; type MoneyReportHeaderStatusBarProps = { /** The next step for the report */ @@ -24,8 +24,11 @@ function MoneyReportHeaderStatusBar({nextStep}: MoneyReportHeaderStatusBarProps) return ( <View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.overflowHidden, styles.w100]}> - <View style={styles.moneyRequestHeaderStatusBarBadge}> - <Text style={[styles.textLabel, styles.textMicroBold]}>{translate(nextStep.title === CONST.NEXT_STEP.FINISHED ? 'iou.finished' : 'iou.nextStep')}</Text> + <View style={[styles.mr3]}> + <Badge + text={translate(nextStep.title === CONST.NEXT_STEP.FINISHED ? 'iou.finished' : 'iou.nextStep')} + badgeStyles={styles.ml0} + /> </View> <View style={[styles.dFlex, styles.flexRow, styles.flexShrink1]}> <RenderHTML html={messageContent} /> diff --git a/src/styles/index.ts b/src/styles/index.ts index 7b79099532fb..e17c3a3f2c1a 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -4305,17 +4305,6 @@ const styles = (theme: ThemeColors) => alignSelf: 'center', }, - moneyRequestHeaderStatusBarBadge: { - width: 68, - height: variables.inputHeightSmall, - borderRadius: variables.componentBorderRadiusSmall, - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - backgroundColor: theme.border, - marginRight: 12, - }, - staticHeaderImage: { minHeight: 240, }, From 6847af46040aed68b6806bcb2c4f4a07f3c1946d Mon Sep 17 00:00:00 2001 From: Sibtain Ali <allroundexperts@gmail.com> Date: Thu, 25 Apr 2024 03:26:02 +0500 Subject: [PATCH 365/580] add transition end check --- src/hooks/useAnimatedHighlightStyle/index.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts index 027741d40b68..4f934fee7652 100644 --- a/src/hooks/useAnimatedHighlightStyle/index.ts +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -1,6 +1,7 @@ import React from 'react'; import {InteractionManager} from 'react-native'; import {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useTheme from '@hooks/useTheme'; import CONST from '@src/CONST'; @@ -49,6 +50,7 @@ export default function useAnimatedHighlightStyle({ }: Props) { const repeatableProgress = useSharedValue(0); const nonRepeatableProgress = useSharedValue(shouldHighlight ? 0 : 1); + const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); const theme = useTheme(); const highlightBackgroundStyle = useAnimatedStyle(() => ({ @@ -59,7 +61,7 @@ export default function useAnimatedHighlightStyle({ })); React.useEffect(() => { - if (!shouldHighlight) { + if (!shouldHighlight || !didScreenTransitionEnd) { return; } @@ -80,7 +82,18 @@ export default function useAnimatedHighlightStyle({ ); })(); }); - }, [shouldHighlight, itemEnterDelay, itemEnterDuration, highlightStartDelay, highlightStartDuration, highlightEndDelay, highlightEndDuration, repeatableProgress, nonRepeatableProgress]); + }, [ + didScreenTransitionEnd, + shouldHighlight, + itemEnterDelay, + itemEnterDuration, + highlightStartDelay, + highlightStartDuration, + highlightEndDelay, + highlightEndDuration, + repeatableProgress, + nonRepeatableProgress, + ]); return highlightBackgroundStyle; } From 0ef5ebc6438d23a054da7db27e534978f05767a1 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 19:40:31 -0400 Subject: [PATCH 366/580] use `withPolicyConnections` to get the connections data to decide if the explanatory text should be displayed --- .../WorkspaceCategoriesSettingsPage.tsx | 92 +++++++++---------- 1 file changed, 43 insertions(+), 49 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 0ebfcde6f97b..4bd715ed13c8 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -1,8 +1,6 @@ -import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -12,66 +10,66 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceRequiresCategory} from '@libs/actions/Policy'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; -type WorkspaceCategoriesSettingsPageOnyxProps = { - /** Collection of categories attached to a policy */ - policyCategories: OnyxEntry<OnyxTypes.PolicyCategories>; -}; +type WorkspaceCategoriesSettingsPageProps = WithPolicyConnectionsProps; -type WorkspaceCategoriesSettingsPageProps = WorkspaceCategoriesSettingsPageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.CATEGORIES_SETTINGS>; - -function WorkspaceCategoriesSettingsPage({route, policyCategories}: WorkspaceCategoriesSettingsPageProps) { +function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSettingsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const isConnectedToAccounting = Object.keys(policy?.connections ?? {}).length > 0; + const policyID = route.params.policyID ?? ''; + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const updateWorkspaceRequiresCategory = (value: boolean) => { - setWorkspaceRequiresCategory(route.params.policyID, value); + setWorkspaceRequiresCategory(policyID, value); }; const hasEnabledOptions = OptionsListUtils.hasEnabledOptions(policyCategories ?? {}); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> + <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> + <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} + policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} > - {({policy}) => ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceCategoriesSettingsPage.displayName} - > - <HeaderWithBackButton title={translate('common.settings')} /> - <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={policy?.errorFields?.requiresCategory} - pendingAction={policy?.pendingFields?.requiresCategory} - errorRowStyles={styles.mh5} - > - <View style={[styles.mt2, styles.mh4]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text style={[styles.textNormal, styles.colorMuted, styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.requiresCategory')}</Text> - <Switch - isOn={policy?.requiresCategory ?? false} - accessibilityLabel={translate('workspace.categories.requiresCategory')} - onToggle={updateWorkspaceRequiresCategory} - disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} - /> - </View> + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceCategoriesSettingsPage.displayName} + > + <HeaderWithBackButton title={translate('common.settings')} /> + <View style={styles.flexGrow1}> + <OfflineWithFeedback + errors={policy?.errorFields?.requiresCategory} + pendingAction={policy?.pendingFields?.requiresCategory} + errorRowStyles={styles.mh5} + > + <View style={[styles.mt2, styles.mh4]}> + <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> + <Text style={[styles.textNormal, styles.colorMuted, styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.requiresCategory')}</Text> + <Switch + isOn={policy?.requiresCategory ?? false} + accessibilityLabel={translate('workspace.categories.requiresCategory')} + onToggle={updateWorkspaceRequiresCategory} + disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} + /> </View> - </OfflineWithFeedback> - </View> - </ScreenWrapper> - )} + </View> + {isConnectedToAccounting && ( + <Text style={[styles.textNormal, styles.colorMuted]}>{`${translate('workspace.categories.importedFromAccountingSoftware')}${translate( + 'workspace.accounting.qbo', + )}`}</Text> + )} + </OfflineWithFeedback> + </View> + </ScreenWrapper> </FeatureEnabledAccessOrNotFoundWrapper> </PaidPolicyAccessOrNotFoundWrapper> </AdminPolicyAccessOrNotFoundWrapper> @@ -80,8 +78,4 @@ function WorkspaceCategoriesSettingsPage({route, policyCategories}: WorkspaceCat WorkspaceCategoriesSettingsPage.displayName = 'WorkspaceCategoriesSettingsPage'; -export default withOnyx<WorkspaceCategoriesSettingsPageProps, WorkspaceCategoriesSettingsPageOnyxProps>({ - policyCategories: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`, - }, -})(WorkspaceCategoriesSettingsPage); +export default withPolicyConnections(WorkspaceCategoriesSettingsPage); From a1cc06f4cec667181469dd2a18bfffee702fabe7 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:28:14 -0400 Subject: [PATCH 367/580] feat: add new UI copy --- src/languages/en.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 728b214d65cf..0af5af59f9f1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2036,6 +2036,7 @@ export default { tagRequiredError: 'Tag name is required.', existingTagError: 'A tag with this name already exists.', genericFailureMessage: 'An error occurred while updating the tag, please try again.', + importedFromAccountingSoftware: 'The tags below are imported from your', }, taxes: { subtitle: 'Add tax names, rates, and set defaults.', From 558174701f53e44e5d999f94a941d17b943af14d Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:30:30 -0400 Subject: [PATCH 368/580] feat: add new Spanish translation --- src/languages/es.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index 6ca7c4a210e1..afaf4f829c94 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2063,6 +2063,7 @@ export default { tagRequiredError: 'Lo nombre de la etiqueta es obligatorio.', existingTagError: 'Ya existe una etiqueta con este nombre.', genericFailureMessage: 'Se produjo un error al actualizar la etiqueta, inténtelo nuevamente.', + importedFromAccountingSoftware: 'Etiquetas importadas desde', }, taxes: { subtitle: 'Añade nombres, tasas y establezca valores por defecto para los impuestos.', From fa8b3fc908c885e1f14bf6e1c7e6b47996a5c343 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:35:23 -0400 Subject: [PATCH 369/580] chore: get current URL --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index fafee7b3b74d..976670286811 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -19,6 +19,7 @@ import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; @@ -71,6 +72,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { const dropdownButtonRef = useRef(null); const [deleteTagsConfirmModalVisible, setDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); + const {environmentURL} = useEnvironment(); const fetchTags = useCallback(() => { Policy.openPolicyTagsPage(route.params.policyID); From 5623a7bb0b4e6363b57e704a9f7229764c4acc81 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:37:00 -0400 Subject: [PATCH 370/580] chore: access policy ID just once --- .../workspace/tags/WorkspaceTagsPage.tsx | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 976670286811..d145c76e8f3c 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -72,11 +72,12 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { const dropdownButtonRef = useRef(null); const [deleteTagsConfirmModalVisible, setDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); + const policyID = route.params.policyID ?? ''; const {environmentURL} = useEnvironment(); const fetchTags = useCallback(() => { - Policy.openPolicyTagsPage(route.params.policyID); - }, [route.params.policyID]); + Policy.openPolicyTagsPage(policyID); + }, [policyID]); const {isOffline} = useNetwork({onReconnect: fetchTags}); @@ -144,22 +145,22 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { ); const navigateToTagsSettings = () => { - Navigation.navigate(ROUTES.WORKSPACE_TAGS_SETTINGS.getRoute(route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_TAGS_SETTINGS.getRoute(policyID)); }; const navigateToCreateTagPage = () => { - Navigation.navigate(ROUTES.WORKSPACE_TAG_CREATE.getRoute(route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_CREATE.getRoute(policyID)); }; const navigateToTagSettings = (tag: PolicyOption) => { - Navigation.navigate(ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(route.params.policyID, tag.keyForList)); + Navigation.navigate(ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(policyID, tag.keyForList)); }; const selectedTagsArray = Object.keys(selectedTags).filter((key) => selectedTags[key]); const handleDeleteTags = () => { setSelectedTags({}); - Policy.deletePolicyTags(route.params.policyID, selectedTagsArray); + Policy.deletePolicyTags(policyID, selectedTagsArray); setDeleteTagsConfirmModalVisible(false); }; @@ -194,7 +195,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.DISABLE, onSelected: () => { setSelectedTags({}); - Policy.setWorkspaceTagEnabled(route.params.policyID, tagsToDisable); + Policy.setWorkspaceTagEnabled(policyID, tagsToDisable); }, }); } @@ -216,7 +217,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { value: CONST.POLICY.TAGS_BULK_ACTION_TYPES.ENABLE, onSelected: () => { setSelectedTags({}); - Policy.setWorkspaceTagEnabled(route.params.policyID, tagsToEnable); + Policy.setWorkspaceTagEnabled(policyID, tagsToEnable); }, }); } @@ -259,10 +260,10 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> - <PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}> + <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> + <PaidPolicyAccessOrNotFoundWrapper policyID={policyID}> <FeatureEnabledAccessOrNotFoundWrapper - policyID={route.params.policyID} + policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} > <ScreenWrapper @@ -319,7 +320,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { customListHeader={getCustomListHeader()} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - onDismissError={(item) => Policy.clearPolicyTagErrors(route.params.policyID, item.value)} + onDismissError={(item) => Policy.clearPolicyTagErrors(policyID, item.value)} /> )} </ScreenWrapper> From 45c94391cd33622ce505f91b90f2bb3cd0088134 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:38:08 -0400 Subject: [PATCH 371/580] refactor: get Onyx value using `useOnyx` --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index d145c76e8f3c..68e1985b1847 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -3,7 +3,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import lodashSortBy from 'lodash/sortBy'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; @@ -63,7 +63,7 @@ type WorkspaceTagsOnyxProps = { type WorkspaceTagsPageProps = WorkspaceTagsOnyxProps & StackScreenProps<WorkspacesCentralPaneNavigatorParamList, typeof SCREENS.WORKSPACE.TAGS>; -function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { +function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const theme = useTheme(); @@ -73,6 +73,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { const [deleteTagsConfirmModalVisible, setDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); const policyID = route.params.policyID ?? ''; + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyId}`); const {environmentURL} = useEnvironment(); const fetchTags = useCallback(() => { From c1905bd8d7318fb41a3d6e631ffcc4088ee45761 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:39:46 -0400 Subject: [PATCH 372/580] replace withOnyx with withPolicyConnections --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 68e1985b1847..3433451ac5ed 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -3,7 +3,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import lodashSortBy from 'lodash/sortBy'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; @@ -33,6 +33,7 @@ import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -333,8 +334,4 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { WorkspaceTagsPage.displayName = 'WorkspaceTagsPage'; -export default withOnyx<WorkspaceTagsPageProps, WorkspaceTagsOnyxProps>({ - policyTags: { - key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${route.params.policyID}`, - }, -})(WorkspaceTagsPage); +export default withPolicyConnections(TagSettingsPage); From b20d67e907ea65b672b4e47b7a652dcebfd6f67d Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:40:38 -0400 Subject: [PATCH 373/580] fix: wrong variable name --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 3433451ac5ed..8013b8b617a5 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -74,7 +74,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const [deleteTagsConfirmModalVisible, setDeleteTagsConfirmModalVisible] = useState(false); const isFocused = useIsFocused(); const policyID = route.params.policyID ?? ''; - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyId}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const {environmentURL} = useEnvironment(); const fetchTags = useCallback(() => { From ecf51d4fdc16008b6266d076021a0895e1194c64 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:44:14 -0400 Subject: [PATCH 374/580] fix: import statements and component name --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 8013b8b617a5..724952a00aca 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -1,10 +1,8 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import type {StackScreenProps} from '@react-navigation/stack'; import lodashSortBy from 'lodash/sortBy'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; @@ -29,16 +27,15 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; -import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; @@ -57,12 +54,7 @@ type PolicyOption = ListItem & { keyForList: string; }; -type WorkspaceTagsOnyxProps = { - /** Collection of tags attached to a policy */ - policyTags: OnyxEntry<OnyxTypes.PolicyTagList>; -}; - -type WorkspaceTagsPageProps = WorkspaceTagsOnyxProps & StackScreenProps<WorkspacesCentralPaneNavigatorParamList, typeof SCREENS.WORKSPACE.TAGS>; +type WorkspaceTagsPageProps = WithPolicyConnectionsProps; function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -334,4 +326,4 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { WorkspaceTagsPage.displayName = 'WorkspaceTagsPage'; -export default withPolicyConnections(TagSettingsPage); +export default withPolicyConnections(WorkspaceTagsPage); From 68e04b7e0e7b3be8834d9036aecfa993b5345a30 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:48:40 -0400 Subject: [PATCH 375/580] feat: display the explanatory text --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 724952a00aca..553f738bce2e 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -16,6 +16,7 @@ import RightElementEnabledStatus from '@components/SelectionList/RightElementEna import TableListItem from '@components/SelectionList/TableListItem'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; +import TextLink from '@components/TextLink'; import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; @@ -56,7 +57,7 @@ type PolicyOption = ListItem & { type WorkspaceTagsPageProps = WithPolicyConnectionsProps; -function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { +function WorkspaceTagsPage({route, policy}: WorkspaceTagsPageProps) { const {isSmallScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const theme = useTheme(); @@ -68,6 +69,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const policyID = route.params.policyID ?? ''; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const {environmentURL} = useEnvironment(); + const isAccountingConnected = Object.keys(policy?.connections ?? {}).length > 0; const fetchTags = useCallback(() => { Policy.openPolicyTagsPage(policyID); @@ -286,7 +288,19 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { /> {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} <View style={[styles.ph5, styles.pb5, styles.pt3]}> - <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.subtitle')}</Text> + {isAccountingConnected ? ( + <Text> + <Text style={[styles.textNormal, styles.colorMuted]}>{`${translate('workspace.tags.importedFromAccountingSoftware')} `}</Text> + <TextLink + style={[styles.textNormal, styles.link]} + href={`${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(policyID)}`} + > + {`${translate('workspace.accounting.qbo')} ${translate('workspace.accounting.settings')}`} + </TextLink> + </Text> + ) : ( + <Text style={[styles.textNormal, styles.colorMuted]}>{translate('workspace.tags.subtitle')}</Text> + )} </View> {isLoading && ( <ActivityIndicator From 57c5350caaed6d89dd6d3930d2d19709fd64eb3c Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Wed, 24 Apr 2024 20:52:05 -0400 Subject: [PATCH 376/580] chore: change variable name --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 553f738bce2e..5c867ef43090 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -69,7 +69,7 @@ function WorkspaceTagsPage({route, policy}: WorkspaceTagsPageProps) { const policyID = route.params.policyID ?? ''; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const {environmentURL} = useEnvironment(); - const isAccountingConnected = Object.keys(policy?.connections ?? {}).length > 0; + const isConnectedToAccounting = Object.keys(policy?.connections ?? {}).length > 0; const fetchTags = useCallback(() => { Policy.openPolicyTagsPage(policyID); @@ -288,7 +288,7 @@ function WorkspaceTagsPage({route, policy}: WorkspaceTagsPageProps) { /> {isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} <View style={[styles.ph5, styles.pb5, styles.pt3]}> - {isAccountingConnected ? ( + {isConnectedToAccounting ? ( <Text> <Text style={[styles.textNormal, styles.colorMuted]}>{`${translate('workspace.tags.importedFromAccountingSoftware')} `}</Text> <TextLink From 84947fc19b83de92c73129611a88f8d5fb02b301 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 12:02:36 +0700 Subject: [PATCH 377/580] Fix crash app and move paid by option to the top --- .../MoneyRequestConfirmationList.tsx | 159 +++++++++--------- src/libs/actions/IOU.ts | 1 + .../step/IOURequestStepConfirmation.tsx | 4 +- .../step/IOURequestStepParticipants.tsx | 4 +- 4 files changed, 86 insertions(+), 82 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 185972e1b596..d8b4df136d7c 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -425,10 +425,13 @@ function MoneyRequestConfirmationList({ const selectedParticipants = useMemo(() => selectedParticipantsProp.filter((participant) => participant.selected), [selectedParticipantsProp]); const payeePersonalDetails = useMemo(() => payeePersonalDetailsProp ?? currentUserPersonalDetails, [payeePersonalDetailsProp, currentUserPersonalDetails]); - const payeeTooltipDetails = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs([payeePersonalDetails.accountID], personalDetails), false); + const payeeTooltipDetails = useMemo( + () => ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs([payeePersonalDetails.accountID], personalDetails), false), + [payeePersonalDetails.accountID, personalDetails], + ); const payeeIcons = [ { - source: UserUtils.getAvatar(payeePersonalDetails.avatar, payeePersonalDetails.accountID), + source: UserUtils.getAvatar(payeePersonalDetails.avatar, payeePersonalDetails.accountID) ?? '', name: payeePersonalDetails.login ?? '', type: CONST.ICON_TYPE_AVATAR, id: payeePersonalDetails.accountID, @@ -685,27 +688,6 @@ function MoneyRequestConfirmationList({ // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ - { - item: ( - <MenuItem - key={translate('moneyRequestConfirmationList.paidBy')} - label={translate('moneyRequestConfirmationList.paidBy')} - interactive={!isPolicyExpenseChat && !isReadOnly} - description={payeePersonalDetails.login ?? ReportUtils.getDisplayNameForParticipant(payeePersonalDetails.accountID)} - title={payeePersonalDetails.displayName ?? ReportUtils.getDisplayNameForParticipant(payeePersonalDetails.accountID)} - icon={payeeIcons} - onPress={() => { - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), - ); - }} - shouldShowRightIcon={!isPolicyExpenseChat && !isReadOnly} - titleWithTooltips={payeePersonalDetails?.isOptimisticPersonalDetail ? undefined : payeeTooltipDetails} - /> - ), - shouldShow: isTypeSplit && action === CONST.IOU.ACTION.CREATE, - isSupplementary: false, - }, { item: ( <MenuItemWithTopDescription @@ -991,64 +973,83 @@ function MoneyRequestConfirmationList({ ); return ( - // @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) - <OptionsSelector - sections={optionSelectorSections} - onSelectRow={canModifyParticipants ? selectParticipant : navigateToReportOrUserDetail} - onAddToSelection={selectParticipant} - onConfirmSelection={confirm} - selectedOptions={selectedOptions} - canSelectMultipleOptions={canModifyParticipants} - disableArrowKeysActions={!canModifyParticipants} - boldStyle - showTitleTooltip - shouldTextInputAppearBelowOptions - shouldShowTextInput={false} - shouldUseStyleForChildren={false} - optionHoveredStyle={canModifyParticipants ? styles.hoveredComponentBG : {}} - footerContent={footerContent} - listStyles={listStyles} - shouldAllowScrollingChildren - > - {isDistanceRequest && ( - <View style={styles.confirmationListMapItem}> - <ConfirmedRoute transaction={transaction ?? ({} as OnyxTypes.Transaction)} /> - </View> - )} - {!isDistanceRequest && - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (receiptImage || receiptThumbnail - ? receiptThumbnailContent - : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(policy) && - !isDistanceRequest && - iouType === CONST.IOU.TYPE.SUBMIT && ( - <ReceiptEmptyState - onPress={() => - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - /> - ))} - {primaryFields} - {!shouldShowAllFields && ( - <ShowMoreButton - containerStyle={[styles.mt1, styles.mb2]} - onPress={toggleShouldExpandFields} + <> + {isTypeSplit && action === CONST.IOU.ACTION.CREATE && ( + <MenuItem + key={translate('moneyRequestConfirmationList.paidBy')} + label={translate('moneyRequestConfirmationList.paidBy')} + interactive={!isPolicyExpenseChat && !isReadOnly} + description={payeePersonalDetails.login ?? ReportUtils.getDisplayNameForParticipant(payeePersonalDetails.accountID)} + title={payeePersonalDetails.displayName ?? ReportUtils.getDisplayNameForParticipant(payeePersonalDetails.accountID)} + icon={payeeIcons} + onPress={() => { + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.getRoute(action, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), + ); + }} + shouldShowRightIcon={!isPolicyExpenseChat && !isReadOnly} + titleWithTooltips={payeePersonalDetails?.isOptimisticPersonalDetail ? undefined : payeeTooltipDetails} /> )} - {shouldShowAllFields && supplementaryFields} - <ConfirmModal - title={translate('attachmentPicker.wrongFileType')} - onConfirm={navigateBack} - onCancel={navigateBack} - isVisible={isAttachmentInvalid} - prompt={translate('attachmentPicker.protectedPDFNotSupported')} - confirmText={translate('common.close')} - shouldShowCancelButton={false} - /> - </OptionsSelector> + {/** @ts-expect-error This component is deprecated and will not be migrated to TypeScript (context: https://expensify.slack.com/archives/C01GTK53T8Q/p1709232289899589?thread_ts=1709156803.359359&cid=C01GTK53T8Q) */} + <OptionsSelector + sections={optionSelectorSections} + onSelectRow={canModifyParticipants ? selectParticipant : navigateToReportOrUserDetail} + onAddToSelection={selectParticipant} + onConfirmSelection={confirm} + selectedOptions={selectedOptions} + canSelectMultipleOptions={canModifyParticipants} + disableArrowKeysActions={!canModifyParticipants} + boldStyle + showTitleTooltip + shouldTextInputAppearBelowOptions + shouldShowTextInput={false} + shouldUseStyleForChildren={false} + optionHoveredStyle={canModifyParticipants ? styles.hoveredComponentBG : {}} + footerContent={footerContent} + listStyles={listStyles} + shouldAllowScrollingChildren + > + {isDistanceRequest && ( + <View style={styles.confirmationListMapItem}> + <ConfirmedRoute transaction={transaction ?? ({} as OnyxTypes.Transaction)} /> + </View> + )} + {!isDistanceRequest && + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (receiptImage || receiptThumbnail + ? receiptThumbnailContent + : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") + PolicyUtils.isPaidGroupPolicy(policy) && + !isDistanceRequest && + iouType === CONST.IOU.TYPE.SUBMIT && ( + <ReceiptEmptyState + onPress={() => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()), + ) + } + /> + ))} + {primaryFields} + {!shouldShowAllFields && ( + <ShowMoreButton + containerStyle={[styles.mt1, styles.mb2]} + onPress={toggleShouldExpandFields} + /> + )} + {shouldShowAllFields && supplementaryFields} + <ConfirmModal + title={translate('attachmentPicker.wrongFileType')} + onConfirm={navigateBack} + onCancel={navigateBack} + isVisible={isAttachmentInvalid} + prompt={translate('attachmentPicker.protectedPDFNotSupported')} + confirmText={translate('common.close')} + shouldShowCancelButton={false} + /> + </OptionsSelector> + </> ); } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 48c5e3c40cc3..f3d35106e6ba 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3443,6 +3443,7 @@ function createSplitsAndOnyxData( const individualSplit = { email, accountID, + isOptimisticAccount: ReportUtils.isOptimisticPersonalDetail(accountID), amount: splitAmount, iouReportID: oneOnOneIOUReport.reportID, chatReportID: oneOnOneChatReport.reportID, diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index fd40a7c1296e..27db6ca89f78 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -89,7 +89,7 @@ function IOURequestStepConfirmation({ return { login: participant?.login ?? '', accountID: participant?.accountID ?? -1, - avatar: UserUtils.getDefaultAvatarURL(participant?.accountID ?? -1), + avatar: Expensicons.FallbackAvatar, displayName: participant?.login ?? '', isOptimisticPersonalDetail: true, }; @@ -136,7 +136,7 @@ function IOURequestStepConfirmation({ } const payeeParticipant: Participant = {accountID: transaction?.splitPayerAccountIDs?.[0], selected: true}; - IOU.setMoneyRequestParticipants_temporaryForRefactor(transaction.transactionID, [payeeParticipant, ...(transaction?.participants ?? [])]); + IOU.setMoneyRequestParticipants(transaction.transactionID, [payeeParticipant, ...(transaction?.participants ?? [])]); // We only want to run it when the component is mounted // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 374d4e9777cf..c99de6236a43 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; @@ -33,7 +34,8 @@ function IOURequestStepParticipants({ }, transaction, }: IOURequestStepParticipantsProps) { - const participants = transaction?.participants; + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const participants = transaction?.participants?.filter((participant) => participant.accountID !== currentUserPersonalDetails.accountID); const {translate} = useLocalize(); const selectedReportID = useRef<string>(reportID); const numberOfParticipants = useRef(participants?.length ?? 0); From a7d846c50ab14e90289750327b210f600b88239f Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 12:05:43 +0700 Subject: [PATCH 378/580] add splitPayerAccountIDs in splitBillAndOpenReport function --- src/libs/actions/IOU.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index f3d35106e6ba..10c1fa3f0008 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3577,6 +3577,7 @@ function splitBillAndOpenReport({ tag = '', billable = false, iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, + splitPayerAccountIDs = [] }: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( @@ -3593,6 +3594,7 @@ function splitBillAndOpenReport({ '', billable, iouRequestType, + splitPayerAccountIDs ); const parameters: SplitBillParams = { From d43d24ab24caf96062141e9e41ad93e8a615f5ba Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 12:06:51 +0700 Subject: [PATCH 379/580] add splitPayerAccountIDs in splitBillAndOpenReport function --- src/libs/actions/IOU.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index f3d35106e6ba..20aaf5655ae7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3577,6 +3577,7 @@ function splitBillAndOpenReport({ tag = '', billable = false, iouRequestType = CONST.IOU.REQUEST_TYPE.MANUAL, + splitPayerAccountIDs = [], }: SplitBillActionsParams) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const {splitData, splits, onyxData} = createSplitsAndOnyxData( @@ -3593,6 +3594,7 @@ function splitBillAndOpenReport({ '', billable, iouRequestType, + splitPayerAccountIDs, ); const parameters: SplitBillParams = { From 749e4d2e839b359c403dbf719e6312697e60100e Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 12:16:08 +0700 Subject: [PATCH 380/580] fix lint --- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 27db6ca89f78..80eb858eb5a8 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -21,7 +21,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import * as UserUtils from '@libs/UserUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From 68afedd275c198a6878821a89a4b7a9d5409b904 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Thu, 25 Apr 2024 09:22:39 +0200 Subject: [PATCH 381/580] Hide tabs on the amount step for invoices --- src/pages/iou/request/IOURequestStartPage.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 5d37c05846c1..b4b945ed6a38 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -103,9 +103,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = - (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT && iouType !== CONST.IOU.TYPE.INVOICE; - const shouldDisplayScanRequest = iouType !== CONST.IOU.TYPE.INVOICE; + const shouldDisplayDistanceRequest = (!!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate) && iouType !== CONST.IOU.TYPE.SPLIT; // Allow the user to submit the expense if we are submitting the expense in global menu or the report can create the exoense const isAllowedToCreateRequest = isEmptyObject(report?.reportID) || ReportUtils.canCreateRequest(report, policy, iouType) || PolicyUtils.canSendInvoice(allPolicies); @@ -146,14 +144,14 @@ function IOURequestStartPage({ title={tabTitles[iouType]} onBackButtonPress={navigateBack} /> - {iouType !== CONST.IOU.TYPE.SEND && iouType !== CONST.IOU.TYPE.PAY ? ( + {iouType !== CONST.IOU.TYPE.SEND && iouType !== CONST.IOU.TYPE.PAY && iouType !== CONST.IOU.TYPE.INVOICE ? ( <OnyxTabNavigator id={CONST.TAB.IOU_REQUEST_TYPE} onTabSelected={resetIOUTypeIfChanged} tabBar={TabSelector} > <TopTab.Screen name={CONST.TAB_REQUEST.MANUAL}>{() => <IOURequestStepAmount route={route} />}</TopTab.Screen> - {shouldDisplayScanRequest && <TopTab.Screen name={CONST.TAB_REQUEST.SCAN}>{() => <IOURequestStepScan route={route} />}</TopTab.Screen>} + <TopTab.Screen name={CONST.TAB_REQUEST.SCAN}>{() => <IOURequestStepScan route={route} />}</TopTab.Screen> {shouldDisplayDistanceRequest && <TopTab.Screen name={CONST.TAB_REQUEST.DISTANCE}>{() => <IOURequestStepDistance route={route} />}</TopTab.Screen>} </OnyxTabNavigator> ) : ( From 3a32eb245076999b7c0ec594853bcb19cd16766e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= <contact@fabiohenriques.dev> Date: Thu, 25 Apr 2024 08:42:37 +0100 Subject: [PATCH 382/580] Address review comments --- src/CONST.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index dbc4b0416b18..c52aed9090b6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -7,6 +7,7 @@ import type {ValueOf} from 'type-fest'; import BankAccount from './libs/models/BankAccount'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; +import type PlaidBankAccount from './types/onyx/PlaidBankAccount'; import type {Unit} from './types/onyx/Policy'; type RateAndUnit = { @@ -1374,7 +1375,7 @@ const CONST = { DEFAULT_DATA: { bankName: '', plaidAccessToken: '', - bankAccounts: [] as [], + bankAccounts: [] as PlaidBankAccount[], isLoading: false, error: '', errors: {}, From 3ec1eb645aab9ada79b77e402bda140fc4e14b96 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 14:52:40 +0700 Subject: [PATCH 383/580] implement alternative solution --- .../MoneyRequestConfirmationList.tsx | 68 ++++++++++++------- src/libs/OptionsListUtils.ts | 9 +-- .../step/IOURequestStepConfirmation.tsx | 12 ---- .../step/IOURequestStepParticipants.tsx | 4 +- .../request/step/IOURequestStepSplitPayer.tsx | 17 ++++- 5 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index d8b4df136d7c..d5cc00bce1cc 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -382,15 +382,9 @@ function MoneyRequestConfirmationList({ * Returns the participants with amount */ const getParticipantsWithAmount = useCallback( - (participantsList: Participant[], payerAccountID: number) => { - const amount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? ''); - const payerAmount = IOUUtils.calculateAmount(participantsList.length - 1, iouAmount, iouCurrencyCode ?? '', true); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( - participantsList, - amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : '', - payerAmount > 0 ? CurrencyUtils.convertToDisplayString(payerAmount, iouCurrencyCode) : '', - payerAccountID, - ); + (participantsList: Participant[]) => { + const amount = IOUUtils.calculateAmount(participantsList.length, iouAmount, iouCurrencyCode ?? ''); + return OptionsListUtils.getIOUConfirmationOptionsFromParticipants(participantsList, amount > 0 ? CurrencyUtils.convertToDisplayString(amount, iouCurrencyCode) : ''); }, [iouAmount, iouCurrencyCode], ); @@ -438,11 +432,12 @@ function MoneyRequestConfirmationList({ }, ]; const canModifyParticipants = !isReadOnly && canModifyParticipantsProp && hasMultipleParticipants; + const shouldDisablePaidBySection = canModifyParticipants; const optionSelectorSections = useMemo(() => { const sections = []; const unselectedParticipants = selectedParticipantsProp.filter((participant) => !participant.selected); if (hasMultipleParticipants) { - const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants, payeePersonalDetails.accountID); + const formattedSelectedParticipants = getParticipantsWithAmount(selectedParticipants); let formattedParticipantsList = [...new Set([...formattedSelectedParticipants, ...unselectedParticipants])]; if (!canModifyParticipants) { @@ -452,11 +447,26 @@ function MoneyRequestConfirmationList({ })); } - sections.push({ - title: translate('moneyRequestConfirmationList.splitAmounts'), - data: formattedParticipantsList, - shouldShow: true, - }); + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants.length, iouAmount, iouCurrencyCode ?? '', true); + + const formattedPayeeOption = OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail( + payeePersonalDetails, + iouAmount > 0 ? CurrencyUtils.convertToDisplayString(myIOUAmount, iouCurrencyCode) : '', + ); + + sections.push( + { + title: translate('moneyRequestConfirmationList.paidBy'), + data: [formattedPayeeOption], + shouldShow: true, + isDisabled: shouldDisablePaidBySection, + }, + { + title: translate('moneyRequestConfirmationList.splitWith'), + data: formattedParticipantsList, + shouldShow: true, + }, + ); } else { const formattedSelectedParticipants = selectedParticipants.map((participant) => ({ ...participant, @@ -469,14 +479,25 @@ function MoneyRequestConfirmationList({ }); } return sections; - }, [selectedParticipants, selectedParticipantsProp, hasMultipleParticipants, getParticipantsWithAmount, translate, canModifyParticipants, payeePersonalDetails.accountID]); + }, [ + selectedParticipants, + selectedParticipantsProp, + hasMultipleParticipants, + iouAmount, + iouCurrencyCode, + getParticipantsWithAmount, + payeePersonalDetails, + translate, + shouldDisablePaidBySection, + canModifyParticipants, + ]); const selectedOptions = useMemo(() => { if (!hasMultipleParticipants) { return []; } - return [...selectedParticipants]; - }, [selectedParticipants, hasMultipleParticipants]); + return [...selectedParticipants, OptionsListUtils.getIOUConfirmationOptionsFromPayeePersonalDetail(payeePersonalDetails)]; + }, [selectedParticipants, hasMultipleParticipants, payeePersonalDetails]); useEffect(() => { if (!isDistanceRequest || isMovingTransactionFromTrackExpense) { @@ -539,12 +560,12 @@ function MoneyRequestConfirmationList({ const selectParticipant = useCallback( (option: Participant) => { // Return early if selected option is currently logged in user. - if (option.accountID === session?.accountID || option.accountID === payeePersonalDetails.accountID) { + if (option.accountID === session?.accountID) { return; } onSelectParticipant?.(option); }, - [session?.accountID, onSelectParticipant, payeePersonalDetails.accountID], + [session?.accountID, onSelectParticipant], ); /** @@ -607,7 +628,7 @@ function MoneyRequestConfirmationList({ playSound(SOUNDS.DONE); setDidConfirm(true); - onConfirm?.(selectedParticipants.filter((participant) => participant.accountID !== currentUserPersonalDetails.accountID)); + onConfirm?.(selectedParticipants); } }, [ @@ -625,7 +646,6 @@ function MoneyRequestConfirmationList({ iouAmount, isEditingSplitBill, onConfirm, - currentUserPersonalDetails.accountID, ], ); @@ -635,7 +655,7 @@ function MoneyRequestConfirmationList({ } const shouldShowSettlementButton = iouType === CONST.IOU.TYPE.PAY; - const shouldDisableButton = isTypeSplit ? selectedParticipants.length === 1 : selectedParticipants.length === 0; + const shouldDisableButton = selectedParticipants.length === 0; const button = shouldShowSettlementButton ? ( <SettlementButton @@ -683,7 +703,7 @@ function MoneyRequestConfirmationList({ {button} </> ); - }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2, isTypeSplit]); + }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a9329b5c07ae..e9f0fc9dc451 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1965,15 +1965,10 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: Person /** * Build the IOUConfirmationOptions for showing participants */ -function getIOUConfirmationOptionsFromParticipants( - participants: Array<Participant | ReportUtils.OptionData>, - amountText: string, - payerAmountText: string, - payerAccountID: number, -): Array<Participant | ReportUtils.OptionData> { +function getIOUConfirmationOptionsFromParticipants(participants: Array<Participant | ReportUtils.OptionData>, amountText: string): Array<Participant | ReportUtils.OptionData> { return participants.map((participant) => ({ ...participant, - descriptiveText: participant.accountID === payerAccountID ? payerAmountText : amountText, + descriptiveText: amountText, })); } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 80eb858eb5a8..fe08d231eae8 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -129,18 +129,6 @@ function IOURequestStepConfirmation({ const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(report)), [report]); const formHasBeenSubmitted = useRef(false); - useEffect(() => { - if (transaction?.participants?.findIndex((participant) => participant.accountID === transaction?.splitPayerAccountIDs?.[0]) !== -1 || iouType !== CONST.IOU.TYPE.SPLIT) { - return; - } - - const payeeParticipant: Participant = {accountID: transaction?.splitPayerAccountIDs?.[0], selected: true}; - IOU.setMoneyRequestParticipants(transaction.transactionID, [payeeParticipant, ...(transaction?.participants ?? [])]); - - // We only want to run it when the component is mounted - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useEffect(() => { const policyExpenseChat = participants?.find((participant) => participant.isPolicyExpenseChat); if (policyExpenseChat?.policyID) { diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index c99de6236a43..374d4e9777cf 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; @@ -34,8 +33,7 @@ function IOURequestStepParticipants({ }, transaction, }: IOURequestStepParticipantsProps) { - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const participants = transaction?.participants?.filter((participant) => participant.accountID !== currentUserPersonalDetails.accountID); + const participants = transaction?.participants; const {translate} = useLocalize(); const selectedReportID = useRef<string>(reportID); const numberOfParticipants = useRef(participants?.length ?? 0); diff --git a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx index dc1a958ed538..d6d542f126f6 100644 --- a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx +++ b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx @@ -3,6 +3,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {usePersonalDetails} from '@components/OnyxProvider'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import * as IOUUtils from '@libs/IOUUtils'; @@ -35,9 +36,21 @@ function IOURequestStepSplitPayer({ const {translate} = useLocalize(); const personalDetails = usePersonalDetails(); const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + + const currentUserOption = useMemo( + () => ({ + accountID: currentUserPersonalDetails.accountID, + searchText: currentUserPersonalDetails.login, + selected: true, + }), + [currentUserPersonalDetails], + ); + const sections = useMemo(() => { + const participants = transaction?.participants ?? []; const participantOptions = - transaction?.participants + [currentUserOption, ...participants] ?.filter((participant) => Boolean(participant.accountID)) ?.map((participant) => { const participantAccountID = participant.accountID ?? 0; @@ -52,7 +65,7 @@ function IOURequestStepSplitPayer({ })), }, ]; - }, [transaction?.participants, personalDetails, transaction?.splitPayerAccountIDs]); + }, [transaction?.participants, personalDetails, transaction?.splitPayerAccountIDs, currentUserOption]); const navigateBack = () => { Navigation.goBack(backTo); From 31b47a713df5022c26aba481c963d4e7c588153f Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Thu, 25 Apr 2024 10:02:14 +0200 Subject: [PATCH 384/580] Fixes after merging main --- src/libs/actions/IOU.ts | 2 +- src/pages/iou/request/step/IOURequestStepSendFrom.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index b3b763f3dcbf..254671eb8eae 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -43,6 +43,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticInviteReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import * as UserUtils from '@libs/UserUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; @@ -3375,7 +3376,6 @@ function sendInvoice( API.write(WRITE_COMMANDS.SEND_INVOICE, parameters, onyxData); - resetMoneyRequestInfo(); Navigation.dismissModal(invoiceRoomReportID); Report.notifyNewAction(invoiceRoomReportID, receiver.accountID); } diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index a64fe2b58054..724909418842 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -74,7 +74,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte selected: false, }); - IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, newParticipants); + IOU.setMoneyRequestParticipants(transactionID, newParticipants); navigateBack(); }; From b47c929047042f05793880ab140d788a849d9dd6 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 15:18:36 +0700 Subject: [PATCH 385/580] fix: remmove custom right icon button --- src/components/Button/index.tsx | 17 ++++++++++++----- .../ButtonWithDropdownMenu/index.tsx | 18 +++--------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 25a9fcbc215a..ce012d650512 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -116,8 +116,8 @@ type ButtonProps = Partial<ChildrenProps> & { /** Boolean whether to display the right icon */ shouldShowRightIcon?: boolean; - /** The custom icon to display to the right of the text */ - customRightIcon?: React.ReactNode; + /** Whether the button should use split style or not */ + isSplitButton?: boolean; }; type KeyboardShortcutComponentProps = Pick<ButtonProps, 'isDisabled' | 'isLoading' | 'onPress' | 'pressOnEnter' | 'allowBubble' | 'enterKeyEventListenerPriority'>; @@ -201,7 +201,7 @@ function Button( id = '', accessibilityLabel = '', - customRightIcon, + isSplitButton = false, ...rest }: ButtonProps, ref: ForwardedRef<View | HTMLDivElement>, @@ -209,7 +209,7 @@ function Button( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - + const renderContent = () => { if ('children' in rest) { return rest.children; @@ -257,7 +257,14 @@ function Button( </View> {shouldShowRightIcon && ( <View style={[styles.justifyContentCenter, large ? styles.ml2 : styles.ml1, iconRightStyles]}> - {customRightIcon ?? ( + {!isSplitButton ? ( + <Icon + src={iconRight} + fill={iconFill ?? (success || danger ? theme.textLight : theme.icon)} + small={medium} + medium={large} + /> + ) : ( <Icon src={iconRight} fill={iconFill ?? (success || danger ? theme.textLight : theme.icon)} diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 1ac73844509b..a28b7ebf0864 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; import Icon from '@components/Icon'; @@ -65,19 +65,6 @@ function ButtonWithDropdownMenu<IValueType>({ }); } }, [windowWidth, windowHeight, isMenuVisible, anchorAlignment.vertical]); - - const getIconRight = useCallback( - () => ( - <Icon - medium={isButtonSizeLarge} - small={!isButtonSizeLarge} - src={Expensicons.DownArrow} - fill={success ? theme.buttonSuccessText : theme.icon} - /> - ), - [isButtonSizeLarge, success, theme.buttonSuccessText, theme.icon], - ); - return ( <View style={wrapperStyle}> {shouldAlwaysShowDropdownMenu || options.length > 1 ? ( @@ -98,8 +85,9 @@ function ButtonWithDropdownMenu<IValueType>({ medium={!isButtonSizeLarge} innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} - customRightIcon={getIconRight()} + iconRight={Expensicons.DownArrow} shouldShowRightIcon={!isSplitButton} + isSplitButton={isSplitButton} /> {isSplitButton && ( From 2ab8f7f159fc7208f97ada978bf1ba31f3ed21e8 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 15:26:55 +0700 Subject: [PATCH 386/580] return participant option directly --- src/pages/iou/request/step/IOURequestStepSplitPayer.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx index d6d542f126f6..c2ee404b1205 100644 --- a/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx +++ b/src/pages/iou/request/step/IOURequestStepSplitPayer.tsx @@ -52,10 +52,7 @@ function IOURequestStepSplitPayer({ const participantOptions = [currentUserOption, ...participants] ?.filter((participant) => Boolean(participant.accountID)) - ?.map((participant) => { - const participantAccountID = participant.accountID ?? 0; - return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant); - }) ?? []; + ?.map((participant) => OptionsListUtils.getParticipantsOption(participant, personalDetails)) ?? []; return [ { title: '', From bd31b1e4de8a0c0e243c1feeb5a0c82bda77fb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Thu, 25 Apr 2024 10:32:48 +0200 Subject: [PATCH 387/580] Fix Podfile --- ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index ed387a8d522f..b59f4ef01158 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2558,7 +2558,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf - Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 + Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d From 09b32d5d412a983001346ca16c3eaa7e89252af4 Mon Sep 17 00:00:00 2001 From: dukenv0307 <dukenv0307@gmail.com> Date: Thu, 25 Apr 2024 16:08:30 +0700 Subject: [PATCH 388/580] fix lint --- ios/Podfile.lock | 4 ++-- src/libs/SidebarUtils.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index acca6566753c..6ef495a65bd3 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2586,8 +2586,8 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf - Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 + Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d -COCOAPODS: 1.14.3 +COCOAPODS: 1.13.0 diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index eb09cd4f7137..f09c2868dfa9 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -6,12 +6,12 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; +import type {ChangeLog} from '@src/types/onyx/OriginalMessage'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import type { ChangeLog } from '@src/types/onyx/OriginalMessage'; import * as CollectionUtils from './CollectionUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; import localeCompare from './LocaleCompare'; @@ -337,8 +337,8 @@ function getOptionData({ lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INVITE_TO_ROOM || lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_FROM_ROOM ) { - const lastActionOriginalMessage = lastAction.actionName ? lastAction?.originalMessage as ChangeLog : null ; - const targetAccountIDs = lastActionOriginalMessage?.targetAccountIDs ?? [] + const lastActionOriginalMessage = lastAction.actionName ? (lastAction?.originalMessage as ChangeLog) : null; + const targetAccountIDs = lastActionOriginalMessage?.targetAccountIDs ?? []; const targetAccountIDsLength = targetAccountIDs.length ?? report.lastMessageHtml?.match(/<mention-user[^>]*><\/mention-user>/g)?.length ?? 0; const verb = lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.INVITE_TO_ROOM || lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INVITE_TO_ROOM From a738a518abdd082ec5ff77bef44e75d170ae33da Mon Sep 17 00:00:00 2001 From: Christoph Pader <chris@margelo.io> Date: Thu, 25 Apr 2024 11:14:35 +0200 Subject: [PATCH 389/580] remove unused line --- src/libs/actions/OnyxUpdateManager/utils/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/OnyxUpdateManager/utils/index.ts b/src/libs/actions/OnyxUpdateManager/utils/index.ts index a04c7e884219..4df22d292d19 100644 --- a/src/libs/actions/OnyxUpdateManager/utils/index.ts +++ b/src/libs/actions/OnyxUpdateManager/utils/index.ts @@ -112,7 +112,6 @@ function validateAndApplyDeferredUpdates(clientLastUpdateID?: number): Promise<v const newLastUpdateIDFromClient = clientLastUpdateID ?? lastUpdateIDAppliedToClient ?? 0; deferredUpdatesProxy.deferredUpdates = {...deferredUpdatesProxy.deferredUpdates, ...updatesAfterGaps}; - // updateDeferredUpdates(newDeferredUpdates); // If lastUpdateIDAppliedToClient got updated in the meantime, we will just retrigger the validation and application of the current deferred updates. if (latestMissingUpdateID <= newLastUpdateIDFromClient) { From 083b48358d377a75851eb8a6371452fbdb3a0ddc Mon Sep 17 00:00:00 2001 From: dukenv0307 <dukenv0307@gmail.com> Date: Thu, 25 Apr 2024 16:19:53 +0700 Subject: [PATCH 390/580] fix lint --- src/libs/SidebarUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index f09c2868dfa9..34c4c98fcc3d 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -337,9 +337,9 @@ function getOptionData({ lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INVITE_TO_ROOM || lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.REMOVE_FROM_ROOM ) { - const lastActionOriginalMessage = lastAction.actionName ? (lastAction?.originalMessage as ChangeLog) : null; + const lastActionOriginalMessage = lastAction?.actionName ? (lastAction?.originalMessage as ChangeLog) : null; const targetAccountIDs = lastActionOriginalMessage?.targetAccountIDs ?? []; - const targetAccountIDsLength = targetAccountIDs.length ?? report.lastMessageHtml?.match(/<mention-user[^>]*><\/mention-user>/g)?.length ?? 0; + const targetAccountIDsLength = targetAccountIDs.length !== 0 ? targetAccountIDs.length : report.lastMessageHtml?.match(/<mention-user[^>]*><\/mention-user>/g)?.length ?? 0; const verb = lastActionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.INVITE_TO_ROOM || lastActionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INVITE_TO_ROOM ? Localize.translate(preferredLocale, 'workspace.invite.invited') From a8a55b3797f24148a155d9cd27710aa290059571 Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Thu, 25 Apr 2024 16:39:52 +0700 Subject: [PATCH 391/580] fix use networkStatus instead of isOffline in useNetwork --- src/hooks/useNetwork.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hooks/useNetwork.ts b/src/hooks/useNetwork.ts index 79c70771d58d..778fed200b7b 100644 --- a/src/hooks/useNetwork.ts +++ b/src/hooks/useNetwork.ts @@ -12,7 +12,8 @@ export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = { const callback = useRef(onReconnect); callback.current = onReconnect; - const {isOffline, networkStatus} = useContext(NetworkContext) ?? {...CONST.DEFAULT_NETWORK_DATA, networkStatus: CONST.NETWORK.NETWORK_STATUS.UNKNOWN}; + const {networkStatus} = useContext(NetworkContext) ?? {...CONST.DEFAULT_NETWORK_DATA, networkStatus: CONST.NETWORK.NETWORK_STATUS.UNKNOWN}; + const isOffline = networkStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE; const prevOfflineStatusRef = useRef(isOffline); useEffect(() => { // If we were offline before and now we are not offline then we just reconnected @@ -29,5 +30,5 @@ export default function useNetwork({onReconnect = () => {}}: UseNetworkProps = { prevOfflineStatusRef.current = isOffline; }, [isOffline]); - return {isOffline: networkStatus === CONST.NETWORK.NETWORK_STATUS.OFFLINE}; + return {isOffline}; } From ae7b35eae518f6d75e1a5f7d92cb4085f08efa8f Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Thu, 25 Apr 2024 11:55:15 +0200 Subject: [PATCH 392/580] fix typo --- src/pages/workspace/AccessOrNotFoundWrapper.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index eb3114eaafae..85c1a28d0501 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -43,9 +43,9 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { featureName?: PolicyFeatureName; }; -type PageNotFoundFallackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID'> & {shouldShowFullScreenFallback: boolean}; +type PageNotFoundFallbackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID'> & {shouldShowFullScreenFallback: boolean}; -function PageNotFoundFallback({policyID, shouldShowFullScreenFallback}: PageNotFoundFallackProps) { +function PageNotFoundFallback({policyID, shouldShowFullScreenFallback}: PageNotFoundFallbackProps) { return shouldShowFullScreenFallback ? ( <FullPageNotFoundView shouldShow From c7a0f12d105341c28aabaccade78f5dfe10e6317 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Thu, 25 Apr 2024 12:18:30 +0200 Subject: [PATCH 393/580] fix ExpensifyCardPage bug --- src/pages/settings/Wallet/ExpensifyCardPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.tsx b/src/pages/settings/Wallet/ExpensifyCardPage.tsx index fa13524310c2..49686e19852c 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage.tsx @@ -85,7 +85,7 @@ function ExpensifyCardPage({ const {isOffline} = useNetwork(); const {translate} = useLocalize(); const shouldDisplayCardDomain = !cardList?.[cardID]?.nameValuePairs?.issuedBy; - const domain = cardList?.[cardID].domainName ?? ''; + const domain = cardList?.[cardID]?.domainName ?? ''; const pageTitle = shouldDisplayCardDomain ? translate('cardPage.expensifyCard') : cardList?.[cardID]?.nameValuePairs?.cardTitle ?? translate('cardPage.expensifyCard'); const [isNotFound, setIsNotFound] = useState(false); From 54cf1f5a0c69cb179d155250093dab640458a501 Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Thu, 25 Apr 2024 12:23:56 +0200 Subject: [PATCH 394/580] move access variants to consts --- src/CONST.ts | 4 ++++ src/pages/workspace/AccessOrNotFoundWrapper.tsx | 5 +++-- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 3 ++- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 2 +- .../accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx | 2 +- .../accounting/qbo/advanced/QuickbooksAdvancedPage.tsx | 2 +- .../qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx | 2 +- .../export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx | 2 +- .../qbo/export/QuickbooksExportConfigurationPage.tsx | 2 +- .../accounting/qbo/export/QuickbooksExportDateSelectPage.tsx | 2 +- .../qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx | 2 +- .../export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx | 2 +- .../export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx | 2 +- .../export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx | 2 +- .../export/QuickbooksPreferredExporterConfigurationPage.tsx | 2 +- .../accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx | 2 +- .../accounting/qbo/import/QuickbooksClassesPage.tsx | 2 +- .../accounting/qbo/import/QuickbooksCustomersPage.tsx | 2 +- .../workspace/accounting/qbo/import/QuickbooksImportPage.tsx | 2 +- .../accounting/qbo/import/QuickbooksLocationsPage.tsx | 2 +- .../workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx | 2 +- src/pages/workspace/categories/CategorySettingsPage.tsx | 2 +- src/pages/workspace/categories/CreateCategoryPage.tsx | 2 +- src/pages/workspace/categories/EditCategoryPage.tsx | 2 +- src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 2 +- .../workspace/categories/WorkspaceCategoriesSettingsPage.tsx | 2 +- src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx | 2 +- .../distanceRates/PolicyDistanceRateDetailsPage.tsx | 2 +- .../workspace/distanceRates/PolicyDistanceRateEditPage.tsx | 2 +- .../workspace/distanceRates/PolicyDistanceRatesPage.tsx | 2 +- .../distanceRates/PolicyDistanceRatesSettingsPage.tsx | 2 +- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 2 +- .../workspace/members/WorkspaceOwnerChangeErrorPage.tsx | 2 +- .../workspace/members/WorkspaceOwnerChangeSuccessPage.tsx | 3 ++- .../workspace/members/WorkspaceOwnerChangeWrapperPage.tsx | 2 +- src/pages/workspace/tags/EditTagPage.tsx | 2 +- src/pages/workspace/tags/TagSettingsPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceCreateTagPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceEditTagsPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 +- src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx | 2 +- src/pages/workspace/taxes/NamePage.tsx | 2 +- src/pages/workspace/taxes/ValuePage.tsx | 2 +- src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx | 2 +- src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx | 2 +- src/pages/workspace/taxes/WorkspaceTaxesPage.tsx | 2 +- .../workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx | 2 +- .../taxes/WorkspaceTaxesSettingsForeignCurrency.tsx | 2 +- src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx | 2 +- .../taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx | 2 +- .../workspace/workflows/WorkspaceWorkflowsPayerPage.tsx | 2 +- 51 files changed, 58 insertions(+), 51 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index f0139d82e614..e06517280c1f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1661,6 +1661,10 @@ const CONST = { JOB_DONE: 'jobDone', }, }, + ACCESS_VARIANTS: { + PAID: 'paid', + ADMIN: 'admin', + }, }, CUSTOM_UNITS: { diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index 85c1a28d0501..e7477b3c05d4 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -8,6 +8,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; @@ -16,8 +17,8 @@ import callOrReturn from '@src/types/utils/callOrReturn'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; const POLICY_ACCESS_VARIANTS = { - PAID: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isPaidGroupPolicy(policy) && !!policy?.isPolicyExpenseChatEnabled, - ADMIN: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isPolicyAdmin(policy), + [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isPaidGroupPolicy(policy) && !!policy?.isPolicyExpenseChatEnabled, + [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry<OnyxTypes.Policy>) => PolicyUtils.isPolicyAdmin(policy), } as const satisfies Record<string, (policy: OnyxTypes.Policy) => boolean>; type PolicyAccessVariant = keyof typeof POLICY_ACCESS_VARIANTS; diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index e904e6848ff2..e3e57db5621c 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -14,6 +14,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type SCREENS from '@src/SCREENS'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -196,7 +197,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} > <ScreenWrapper diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 3131b3224eb8..f15accdd15e8 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -174,7 +174,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx index 4400da663f37..15efb97e9d7f 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAccountSelectPage.tsx @@ -69,7 +69,7 @@ function QuickbooksAccountSelectPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx index 167d0e856e61..1c11d23ac0ea 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx @@ -68,7 +68,7 @@ function QuickbooksAdvancedPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx index 8c112b457a14..c22dcc5c8d4e 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksInvoiceAccountSelectPage.tsx @@ -71,7 +71,7 @@ function QuickbooksInvoiceAccountSelectPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx index b35336d8f1d0..a93649f4d64d 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectPage.tsx @@ -72,7 +72,7 @@ function QuickbooksCompanyCardExpenseAccountSelectPage({policy}: WithPolicyProps return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper testID={QuickbooksCompanyCardExpenseAccountSelectPage.displayName}> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx index cb6caf31ea4a..086f4d126843 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx @@ -78,7 +78,7 @@ function QuickbooksExportConfigurationPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx index bcba3b0236f9..2959f40babb4 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx @@ -45,7 +45,7 @@ function QuickbooksExportDateSelectPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx index e7d2c1c11975..7c497cfa6b9f 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportInvoiceAccountSelectPage.tsx @@ -50,7 +50,7 @@ function QuickbooksExportInvoiceAccountSelectPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper testID={QuickbooksExportInvoiceAccountSelectPage.displayName}> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx index f91282511f53..3f97ad68acaf 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseAccountSelectPage.tsx @@ -66,7 +66,7 @@ function QuickbooksOutOfPocketExpenseAccountSelectPage({policy}: WithPolicyProps return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper testID={QuickbooksOutOfPocketExpenseAccountSelectPage.displayName}> diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx index 768a3dff1743..7caf6e8f8336 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseConfigurationPage.tsx @@ -28,7 +28,7 @@ function QuickbooksOutOfPocketExpenseConfigurationPage({policy}: WithPolicyProps return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx index f2224899df53..4b0bce05669b 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksOutOfPocketExpenseEntitySelectPage.tsx @@ -83,7 +83,7 @@ function QuickbooksOutOfPocketExpenseEntitySelectPage({policy}: WithPolicyProps) return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksPreferredExporterConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksPreferredExporterConfigurationPage.tsx index af196a077035..4e782db6fcb0 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksPreferredExporterConfigurationPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksPreferredExporterConfigurationPage.tsx @@ -56,7 +56,7 @@ function QuickBooksExportPreferredExporterPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper policyID={policyID} - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > <ScreenWrapper testID={QuickBooksExportPreferredExporterPage.displayName}> diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx index 1515a8933248..96cb0115d29a 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksChartOfAccountsPage.tsx @@ -24,7 +24,7 @@ function QuickbooksChartOfAccountsPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx index a07f173bd082..d0cd62d043e9 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx @@ -26,7 +26,7 @@ function QuickbooksClassesPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx index 78c2d29d9771..e0543a45e85d 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx @@ -25,7 +25,7 @@ function QuickbooksCustomersPage({policy}: WithPolicyProps) { const isReportFieldsSelected = syncCustomers === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx index ca925d85db68..3d5c4a3d4d45 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksImportPage.tsx @@ -68,7 +68,7 @@ function QuickbooksImportPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksLocationsPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksLocationsPage.tsx index 83044e36032c..b74ddbf28e3c 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksLocationsPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksLocationsPage.tsx @@ -26,7 +26,7 @@ function QuickbooksLocationsPage({policy}: WithPolicyProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx index 8df4644cb873..5112ad127681 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksTaxesPage.tsx @@ -23,7 +23,7 @@ function QuickbooksTaxesPage({policy}: WithPolicyProps) { const isSwitchOn = Boolean(syncTaxes && syncTaxes !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} > diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 6bde0a156704..21741a417a26 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -70,7 +70,7 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} > diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index 994bdbe88f93..752ab5b53cfa 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -37,7 +37,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} > diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx index 5ae11da1f541..201329b539a9 100644 --- a/src/pages/workspace/categories/EditCategoryPage.tsx +++ b/src/pages/workspace/categories/EditCategoryPage.tsx @@ -59,7 +59,7 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} > diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 9cbe509019e1..82f7489548f7 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -248,7 +248,7 @@ function WorkspaceCategoriesPage({policy, route}: WorkspaceCategoriesPageProps) return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyId} featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} > diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index c377b8c3505d..69057227648d 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -37,7 +37,7 @@ function WorkspaceCategoriesSettingsPage({route, policyCategories}: WorkspaceCat const hasEnabledOptions = OptionsListUtils.hasEnabledOptions(policyCategories ?? {}); return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED} > diff --git a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx index 3e16d49284fa..a103d4b61e09 100644 --- a/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx +++ b/src/pages/workspace/distanceRates/CreateDistanceRatePage.tsx @@ -59,7 +59,7 @@ function CreateDistanceRatePage({policy, route}: CreateDistanceRatePageProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} > diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx index 3e2558385266..a75fc74c33cc 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateDetailsPage.tsx @@ -92,7 +92,7 @@ function PolicyDistanceRateDetailsPage({policy, route}: PolicyDistanceRateDetail return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} > diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx index 9517c94dfaa4..6c853557e01a 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx @@ -56,7 +56,7 @@ function PolicyDistanceRateEditPage({policy, route}: PolicyDistanceRateEditPageP return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} > diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 0127de46feef..f1e4b05e1445 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -265,7 +265,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} > diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx index 4af3d96da8a7..d756084e5e49 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx @@ -67,7 +67,7 @@ function PolicyDistanceRatesSettingsPage({policy, policyCategories, route}: Poli return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED} > diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 5dc8d83525e6..ebcd07edbe07 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -127,7 +127,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} > <ScreenWrapper testID={WorkspaceMemberDetailsPage.displayName}> diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx index 0bae645069c5..7f0d14801b66 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeErrorPage.tsx @@ -36,7 +36,7 @@ function WorkspaceOwnerChangeErrorPage({route}: WorkspaceOwnerChangeSuccessPageP return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} > <ScreenWrapper testID={WorkspaceOwnerChangeErrorPage.displayName}> diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx index 7a5ee8a971b7..1460808cd078 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeSuccessPage.tsx @@ -10,6 +10,7 @@ import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as PolicyActions from '@userActions/Policy'; +import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -30,7 +31,7 @@ function WorkspaceOwnerChangeSuccessPage({route}: WorkspaceOwnerChangeSuccessPag return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} > <ScreenWrapper testID={WorkspaceOwnerChangeSuccessPage.displayName}> diff --git a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx index a77ac71f4993..16afaa147157 100644 --- a/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx +++ b/src/pages/workspace/members/WorkspaceOwnerChangeWrapperPage.tsx @@ -55,7 +55,7 @@ function WorkspaceOwnerChangeWrapperPage({route, policy}: WorkspaceOwnerChangeWr return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} > <ScreenWrapper testID={WorkspaceOwnerChangeWrapperPage.displayName}> diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx index a3945a8a5131..98b681955169 100644 --- a/src/pages/workspace/tags/EditTagPage.tsx +++ b/src/pages/workspace/tags/EditTagPage.tsx @@ -68,7 +68,7 @@ function EditTagPage({route, policyTags}: EditTagPageProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} > diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx index f915722f0d1b..ed88a1a9b636 100644 --- a/src/pages/workspace/tags/TagSettingsPage.tsx +++ b/src/pages/workspace/tags/TagSettingsPage.tsx @@ -66,7 +66,7 @@ function TagSettingsPage({route, policyTags}: TagSettingsPageProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} > diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx index ffc8f3e2c33e..5ff60afe12f1 100644 --- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx @@ -68,7 +68,7 @@ function CreateTagPage({route, policyTags}: CreateTagPageProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} > diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index ee50e3bd4c95..fed48c5fe4be 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -56,7 +56,7 @@ function WorkspaceEditTagsPage({route, policyTags}: WorkspaceEditTagsPageProps) return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} > diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 328ed8548975..a32f8d0e7fea 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -256,7 +256,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} > diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index 8c26d80b8ed5..90b8dc16952a 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -41,7 +41,7 @@ function WorkspaceTagsSettingsPage({route, policyTags}: WorkspaceTagsSettingsPag ); return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED} > diff --git a/src/pages/workspace/taxes/NamePage.tsx b/src/pages/workspace/taxes/NamePage.tsx index f42d6f7c9a21..dfcad933b7b0 100644 --- a/src/pages/workspace/taxes/NamePage.tsx +++ b/src/pages/workspace/taxes/NamePage.tsx @@ -68,7 +68,7 @@ function NamePage({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx index c241ee8fba1b..d73e5997a5ed 100644 --- a/src/pages/workspace/taxes/ValuePage.tsx +++ b/src/pages/workspace/taxes/ValuePage.tsx @@ -54,7 +54,7 @@ function ValuePage({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx index 121d32e1662b..ab1b2f252f1e 100644 --- a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx @@ -62,7 +62,7 @@ function WorkspaceCreateTaxPage({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index d4f46fbc2dd6..8ccff7b4e126 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -74,7 +74,7 @@ function WorkspaceEditTaxPage({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index d1b6ad50bf3d..a8e94928709d 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -237,7 +237,7 @@ function WorkspaceTaxesPage({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx index d5cb1c9d31e9..9b65a7b44138 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx @@ -54,7 +54,7 @@ function WorkspaceTaxesSettingsCustomTaxName({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx index ebc756da5622..b006732aec7b 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx @@ -43,7 +43,7 @@ function WorkspaceTaxesSettingsForeignCurrency({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx index bb0f48f2daae..bfd688facc5f 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx @@ -54,7 +54,7 @@ function WorkspaceTaxesSettingsPage({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx index 1113ed3268a8..e1ecfbde3a37 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx @@ -39,7 +39,7 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({ return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx index dc29f2e44fc0..ae4fd680c29c 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -166,7 +166,7 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR return ( <AccessOrNotFoundWrapper - accessVariants={['ADMIN', 'PAID']} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} policyID={route.params.policyID} > <FullPageNotFoundView From 073cc3f4839716a8a9f127e0a6c5eea97a978258 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 17:24:35 +0700 Subject: [PATCH 395/580] fix: refactor not found page in workspace page --- .../AdminPolicyAccessOrNotFoundWrapper.tsx | 14 +- src/pages/workspace/WorkspaceInitialPage.tsx | 22 +- .../workspace/WorkspaceInviteMessagePage.tsx | 22 +- src/pages/workspace/WorkspaceInvitePage.tsx | 28 +- src/pages/workspace/WorkspaceMembersPage.tsx | 74 ++--- .../workspace/WorkspacePageWithSections.tsx | 53 ++-- .../WorkspaceProfileCurrencyPage.tsx | 43 +-- .../WorkspaceProfileDescriptionPage.tsx | 109 +++---- src/pages/workspace/WorkspaceProfilePage.tsx | 287 +++++++++--------- .../workspace/WorkspaceProfileSharePage.tsx | 93 +++--- 10 files changed, 383 insertions(+), 362 deletions(-) diff --git a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx index 1fc4b51aca21..e658522ebd5a 100644 --- a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx @@ -9,6 +9,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Policy from '@userActions/Policy'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; @@ -28,6 +29,15 @@ type AdminPolicyAccessOrNotFoundComponentProps = AdminAccessOrNotFoundOnyxProps /** The report currently being looked at */ policyID: string; + + /** Function to call when pressing the navigation link */ + onLinkPress?: () => void; + + /** The key in the translations file to use for the subtitle */ + subtitleKey?: TranslationPaths; + + /** If true, child components are replaced with a blocking "not found" view */ + shouldShow?: boolean; }; function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFoundComponentProps) { @@ -58,9 +68,11 @@ function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFound return ( <ScreenWrapper testID={AdminPolicyAccessOrNotFoundComponent.displayName}> <FullPageNotFoundView - shouldShow + shouldShow={props.shouldShow ?? true} shouldForceFullScreen onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} + onLinkPress={props.onLinkPress} + subtitleKey={props.subtitleKey} /> </ScreenWrapper> ); diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 2f32034391a5..013ea61e7045 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -5,7 +5,6 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import HighlightableMenuItem from '@components/HighlightableMenuItem'; @@ -35,6 +34,7 @@ import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -283,15 +283,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }, [policy]); return ( - <ScreenWrapper - testID={WorkspaceInitialPage.displayName} - includeSafeAreaPaddingBottom={false} + <AdminPolicyAccessOrNotFoundWrapper + shouldShow={shouldShowNotFoundPage} + onLinkPress={Navigation.resetToHome} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + policyID={policy?.id ?? ''} > - <FullPageNotFoundView - onBackButtonPress={Navigation.dismissModal} - onLinkPress={Navigation.resetToHome} - shouldShow={shouldShowNotFoundPage} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + <ScreenWrapper + testID={WorkspaceInitialPage.displayName} + includeSafeAreaPaddingBottom={false} > <HeaderWithBackButton title={policyName} @@ -343,8 +343,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc cancelText={translate('common.cancel')} danger /> - </FullPageNotFoundView> - </ScreenWrapper> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 30d66662b996..e1859d8a1917 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -6,7 +6,6 @@ import {Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors} from '@components/Form/types'; @@ -36,6 +35,7 @@ import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceInviteMessageForm'; import type {InvitedEmailsToAccountIDs, PersonalDetailsList} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import SearchInputManager from './SearchInputManager'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -132,15 +132,15 @@ function WorkspaceInviteMessagePage({ const policyName = policy?.name; return ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - testID={WorkspaceInviteMessagePage.displayName} + <AdminPolicyAccessOrNotFoundWrapper + policyID={route.params.policyID} + shouldShow={isEmptyObject(policy) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + onLinkPress={PolicyUtils.goBackFromInvalidPolicy} > - <FullPageNotFoundView - shouldShow={isEmptyObject(policy) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - onBackButtonPress={PolicyUtils.goBackFromInvalidPolicy} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + testID={WorkspaceInviteMessagePage.displayName} > <HeaderWithBackButton title={translate('workspace.inviteMessage.inviteMessageTitle')} @@ -215,8 +215,8 @@ function WorkspaceInviteMessagePage({ /> </View> </FormProvider> - </FullPageNotFoundView> - </ScreenWrapper> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 0a9c8cd71894..53194008688d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -3,7 +3,6 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import type {SectionListData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {useOptionsList} from '@components/OptionListContextProvider'; @@ -33,6 +32,7 @@ import type SCREENS from '@src/SCREENS'; import type {Beta, InvitedEmailsToAccountIDs} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import SearchInputManager from './SearchInputManager'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -280,18 +280,18 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli ); return ( - <ScreenWrapper - shouldEnableMaxHeight - shouldUseCachedViewportHeight - testID={WorkspaceInvitePage.displayName} - includeSafeAreaPaddingBottom={false} - onEntryTransitionEnd={() => setDidScreenTransitionEnd(true)} + <AdminPolicyAccessOrNotFoundWrapper + policyID={route.params.policyID} + shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + onLinkPress={PolicyUtils.goBackFromInvalidPolicy} > - <FullPageNotFoundView - shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - onBackButtonPress={PolicyUtils.goBackFromInvalidPolicy} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + <ScreenWrapper + shouldEnableMaxHeight + shouldUseCachedViewportHeight + testID={WorkspaceInvitePage.displayName} + includeSafeAreaPaddingBottom={false} + onEntryTransitionEnd={() => setDidScreenTransitionEnd(true)} > <HeaderWithBackButton title={translate('workspace.invite.invitePeople')} @@ -321,8 +321,8 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} footerContent={footerContent} /> - </FullPageNotFoundView> - </ScreenWrapper> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 30d92363afcd..392cbfb424b5 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -1,16 +1,15 @@ -import {useIsFocused} from '@react-navigation/native'; -import type {StackScreenProps} from '@react-navigation/stack'; +import { useIsFocused } from '@react-navigation/native'; +import type { StackScreenProps } from '@react-navigation/stack'; import lodashIsEqual from 'lodash/isEqual'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {TextInput} from 'react-native'; -import {InteractionManager, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import type { TextInput } from 'react-native'; +import { InteractionManager, View } from 'react-native'; +import type { OnyxEntry } from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; import Badge from '@components/Badge'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; -import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; +import type { DropdownOption, WorkspaceMemberBulkActionType } from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -19,9 +18,9 @@ import MessagesRow from '@components/MessagesRow'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import TableListItem from '@components/SelectionList/TableListItem'; -import type {ListItem, SelectionListHandle} from '@components/SelectionList/types'; +import type { ListItem, SelectionListHandle } from '@components/SelectionList/types'; import Text from '@components/Text'; -import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import type { WithCurrentUserPersonalDetailsProps } from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -32,7 +31,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import type { WorkspacesCentralPaneNavigatorParamList } from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -42,11 +41,12 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session} from '@src/types/onyx'; -import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; +import type { InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session } from '@src/types/onyx'; +import type { Errors, PendingAction } from '@src/types/onyx/OnyxCommon'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; +import type { WithPolicyAndFullscreenLoadingProps } from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; type WorkspaceMembersPageOnyxProps = { /** Session info for the currently logged in user. */ @@ -69,24 +69,24 @@ function invertObject(object: Record<string, string>): Record<string, string> { return inverted; } -type MemberOption = Omit<ListItem, 'accountID'> & {accountID: number}; +type MemberOption = Omit<ListItem, 'accountID'> & { accountID: number }; -function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true}: WorkspaceMembersPageProps) { +function WorkspaceMembersPage({ personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true }: WorkspaceMembersPageProps) { const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList), [policy?.employeeList]); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [selectedEmployees, setSelectedEmployees] = useState<number[]>([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const [errors, setErrors] = useState({}); - const {isOffline} = useNetwork(); + const { isOffline } = useNetwork(); const prevIsOffline = usePrevious(isOffline); const accountIDs = useMemo(() => Object.values(policyMemberEmailsToAccountIDs ?? {}).map((accountID) => Number(accountID)), [policyMemberEmailsToAccountIDs]); const prevAccountIDs = usePrevious(accountIDs); const textInputRef = useRef<TextInput>(null); const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline; const prevPersonalDetails = usePrevious(personalDetails); - const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); - const {isSmallScreenWidth} = useWindowDimensions(); + const { translate, formatPhoneNumber, preferredLocale } = useLocalize(); + const { isSmallScreenWidth } = useWindowDimensions(); const dropdownButtonRef = useRef(null); const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const isLoading = useMemo( @@ -413,7 +413,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, <MessagesRow type="success" // eslint-disable-next-line @typescript-eslint/naming-convention - messages={{0: 'workspace.people.addedWithPrimary'}} + messages={{ 0: 'workspace.people.addedWithPrimary' }} containerStyles={[styles.pb5, styles.ph5]} onClose={() => Policy.dismissAddedWithPrimaryLoginMessages(policyID)} /> @@ -500,7 +500,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, <ButtonWithDropdownMenu<WorkspaceMemberBulkActionType> shouldAlwaysShowDropdownMenu pressOnEnter - customText={translate('workspace.common.selected', {selectedNumber: selectedEmployees.length})} + customText={translate('workspace.common.selected', { selectedNumber: selectedEmployees.length })} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} onPress={() => null} options={getBulkActionsButtonOptions()} @@ -523,17 +523,17 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, }; return ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceMembersPage.displayName} - shouldShowOfflineIndicatorInWideScreen + <AdminPolicyAccessOrNotFoundWrapper + shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || PolicyUtils.isPendingDeletePolicy(policy)} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + policyID={policyID} > - <FullPageNotFoundView - shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || PolicyUtils.isPendingDeletePolicy(policy)} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - onBackButtonPress={PolicyUtils.goBackFromInvalidPolicy} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceMembersPage.displayName} + shouldShowOfflineIndicatorInWideScreen > <HeaderWithBackButton title={translate('workspace.common.members')} @@ -569,7 +569,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, <SelectionList ref={selectionListRef} canSelectMultiple={isPolicyAdmin} - sections={[{data, isDisabled: false}]} + sections={[{ data, isDisabled: false }]} ListItem={TableListItem} disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} @@ -586,8 +586,8 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> </View> - </FullPageNotFoundView> - </ScreenWrapper> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> ); } @@ -597,7 +597,7 @@ export default withCurrentUserPersonalDetails( withPolicyAndFullscreenLoading( withOnyx<WorkspaceMembersPageProps, WorkspaceMembersPageOnyxProps>({ invitedEmailsToAccountIDsDraft: { - key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, + key: ({ route }) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, }, session: { key: ONYXKEYS.SESSION, diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 4889c1dbe350..e3012f9053e9 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -1,10 +1,9 @@ -import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import type {ReactNode} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import { useFocusEffect, useIsFocused } from '@react-navigation/native'; +import type { ReactNode } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import { View } from 'react-native'; +import type { OnyxEntry } from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -19,12 +18,13 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; -import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type { Route } from '@src/ROUTES'; +import type { Policy, ReimbursementAccount, User } from '@src/types/onyx'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; +import type { WithPolicyAndFullscreenLoadingProps } from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; type WorkspacePageWithSectionsOnyxProps = { /** From Onyx */ @@ -117,7 +117,7 @@ function WorkspacePageWithSections({ }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); const policyID = route.params?.policyID ?? ''; - useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)}); + useNetwork({ onReconnect: () => fetchData(policyID, shouldSkipVBBACall) }); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true; @@ -125,7 +125,7 @@ function WorkspacePageWithSections({ const isUsingECard = user?.isUsingExpensifyCard ?? false; const hasVBA = achState === BankAccount.STATE.OPEN; const content = typeof children === 'function' ? children(hasVBA, policyID, isUsingECard) : children; - const {isSmallScreenWidth} = useWindowDimensions(); + const { isSmallScreenWidth } = useWindowDimensions(); const firstRender = useRef(true); const isFocused = useIsFocused(); const prevPolicy = usePrevious(policy); @@ -156,20 +156,19 @@ function WorkspacePageWithSections({ }, [policy, shouldShowNonAdmin]); return ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnablePickerAvoiding={false} - shouldEnableMaxHeight - testID={WorkspacePageWithSections.displayName} - shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow} + <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID ?? ''} + onLinkPress={Navigation.resetToHome} + shouldShow={shouldShow} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > - <FullPageNotFoundView - onBackButtonPress={Navigation.dismissModal} - onLinkPress={Navigation.resetToHome} - shouldShow={shouldShow} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - shouldForceFullScreen + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnablePickerAvoiding={false} + shouldEnableMaxHeight + testID={WorkspacePageWithSections.displayName} + shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow} > + <HeaderWithBackButton title={headerText} guidesCallTaskID={guidesCallTaskID} @@ -195,8 +194,8 @@ function WorkspacePageWithSections({ {footer} </> )} - </FullPageNotFoundView> - </ScreenWrapper> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index c8640d3f71b0..27068583b91c 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -1,7 +1,6 @@ -import React, {useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import React, { useState } from 'react'; +import type { OnyxEntry } from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -11,10 +10,11 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {CurrencyList} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; +import type { CurrencyList } from '@src/types/onyx'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; +import type { WithPolicyAndFullscreenLoadingProps } from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; type WorkspaceProfileCurrentPageOnyxProps = { /** Constant, list of available currencies */ @@ -31,8 +31,8 @@ type WorkspaceProfileCurrencyPageSectionItem = { const getDisplayText = (currencyCode: string, currencySymbol: string) => `${currencyCode} - ${currencySymbol}`; -function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingReportData = true}: WorkspaceProfileCurrentPageProps) { - const {translate} = useLocalize(); +function WorkspaceProfileCurrencyPage({ currencyList = {}, policy, isLoadingReportData = true }: WorkspaceProfileCurrentPageProps) { + const { translate } = useLocalize(); const [searchText, setSearchText] = useState(''); const trimmedText = searchText.trim().toLowerCase(); const currencyListKeys = Object.keys(currencyList ?? {}); @@ -61,7 +61,7 @@ function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingRepor }; }); - const sections = [{data: currencyItems}]; + const sections = [{ data: currencyItems }]; const headerMessage = searchText.trim() && !currencyItems.length ? translate('common.noResultsFound') : ''; @@ -71,15 +71,15 @@ function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingRepor }; return ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - testID={WorkspaceProfileCurrencyPage.displayName} + <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''} + onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > - <FullPageNotFoundView - onBackButtonPress={PolicyUtils.goBackFromInvalidPolicy} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} - shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + testID={WorkspaceProfileCurrencyPage.displayName} > <HeaderWithBackButton title={translate('workspace.editor.currencyInputLabel')} @@ -97,8 +97,9 @@ function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingRepor initiallyFocusedOptionKey={initiallyFocusedOptionKey} showScrollIndicator /> - </FullPageNotFoundView> - </ScreenWrapper> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> + ); } @@ -106,6 +107,6 @@ WorkspaceProfileCurrencyPage.displayName = 'WorkspaceProfileCurrencyPage'; export default withPolicyAndFullscreenLoading( withOnyx<WorkspaceProfileCurrentPageProps, WorkspaceProfileCurrentPageOnyxProps>({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, + currencyList: { key: ONYXKEYS.CURRENCY_LIST }, })(WorkspaceProfileCurrencyPage), ); diff --git a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx index 8086f2414e42..1c8de7ca2aab 100644 --- a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx +++ b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx @@ -1,13 +1,13 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {useCallback, useState} from 'react'; -import {Keyboard, View} from 'react-native'; +import React, { useCallback, useState } from 'react'; +import { Keyboard, View } from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {FormOnyxValues} from '@components/Form/types'; +import type { FormOnyxValues } from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; -import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import type { BaseTextInputRef } from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -17,25 +17,26 @@ import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import withPolicy from './withPolicy'; -import type {WithPolicyProps} from './withPolicy'; +import type { WithPolicyProps } from './withPolicy'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; type Props = WithPolicyProps; const parser = new ExpensiMark(); -function WorkspaceProfileDescriptionPage({policy}: Props) { +function WorkspaceProfileDescriptionPage({ policy }: Props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); + const { translate } = useLocalize(); const [description, setDescription] = useState(() => parser.htmlToMarkdown( // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing policy?.description || - parser.replace( - translate('workspace.common.welcomeNote', { - workspaceName: policy?.name ?? '', - }), - ), + parser.replace( + translate('workspace.common.welcomeNote', { + workspaceName: policy?.name ?? '', + }), + ), ), ); @@ -47,7 +48,7 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { const errors = {}; if (values.description.length > CONST.DESCRIPTION_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]); + ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', { length: values.description.length, limit: CONST.DESCRIPTION_LIMIT }]); } return errors; @@ -67,47 +68,49 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { ); return ( - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnableMaxHeight - testID={WorkspaceProfileDescriptionPage.displayName} - > - <HeaderWithBackButton - title={translate('workspace.editor.descriptionInputLabel')} - onBackButtonPress={() => Navigation.goBack()} - /> - - <FormProvider - formID={ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM} - submitButtonText={translate('workspace.editor.save')} - style={[styles.flexGrow1, styles.ph5]} - scrollContextEnabled - onSubmit={submit} - validate={validate} - enabledWhenOffline + <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''}> + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnableMaxHeight + testID={WorkspaceProfileDescriptionPage.displayName} > - <View style={styles.mb4}> - <InputWrapper - InputComponent={TextInput} - role={CONST.ROLE.PRESENTATION} - inputID="description" - label={translate('workspace.editor.descriptionInputLabel')} - accessibilityLabel={translate('workspace.editor.descriptionInputLabel')} - value={description} - maxLength={CONST.REPORT_DESCRIPTION.MAX_LENGTH} - spellCheck={false} - autoFocus - onChangeText={setDescription} - autoGrowHeight - isMarkdownEnabled - ref={(el: BaseTextInputRef | null): void => { - updateMultilineInputRange(el); - }} - containerStyles={[styles.autoGrowHeightMultilineInput]} - /> - </View> - </FormProvider> - </ScreenWrapper> + <HeaderWithBackButton + title={translate('workspace.editor.descriptionInputLabel')} + onBackButtonPress={() => Navigation.goBack()} + /> + + <FormProvider + formID={ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM} + submitButtonText={translate('workspace.editor.save')} + style={[styles.flexGrow1, styles.ph5]} + scrollContextEnabled + onSubmit={submit} + validate={validate} + enabledWhenOffline + > + <View style={styles.mb4}> + <InputWrapper + InputComponent={TextInput} + role={CONST.ROLE.PRESENTATION} + inputID="description" + label={translate('workspace.editor.descriptionInputLabel')} + accessibilityLabel={translate('workspace.editor.descriptionInputLabel')} + value={description} + maxLength={CONST.REPORT_DESCRIPTION.MAX_LENGTH} + spellCheck={false} + autoFocus + onChangeText={setDescription} + autoGrowHeight + isMarkdownEnabled + ref={(el: BaseTextInputRef | null): void => { + updateMultilineInputRange(el); + }} + containerStyles={[styles.autoGrowHeightMultilineInput]} + /> + </View> + </FormProvider> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index b73c6c0d7062..ba3cfe431496 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -1,9 +1,9 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, {useCallback, useState} from 'react'; -import type {ImageStyle, StyleProp} from 'react-native'; -import {Image, StyleSheet, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import React, { useCallback, useState } from 'react'; +import type { ImageStyle, StyleProp } from 'react-native'; +import { Image, StyleSheet, View } from 'react-native'; +import type { OnyxEntry } from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; import Avatar from '@components/Avatar'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import Button from '@components/Button'; @@ -30,10 +30,11 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; import withPolicy from './withPolicy'; -import type {WithPolicyProps} from './withPolicy'; +import type { WithPolicyProps } from './withPolicy'; import WorkspacePageWithSections from './WorkspacePageWithSections'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; type WorkSpaceProfilePageOnyxProps = { /** Constant, list of available currencies */ @@ -44,12 +45,12 @@ type WorkSpaceProfilePageProps = WithPolicyProps & WorkSpaceProfilePageOnyxProps const parser = new ExpensiMark(); -function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfilePageProps) { +function WorkspaceProfilePage({ policy, currencyList = {}, route }: WorkSpaceProfilePageProps) { const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {isSmallScreenWidth} = useWindowDimensions(); + const { translate } = useLocalize(); + const { isSmallScreenWidth } = useWindowDimensions(); const illustrations = useThemeIllustrations(); - const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); + const { activeWorkspaceID, setActiveWorkspaceID } = useActiveWorkspace(); const outputCurrency = policy?.outputCurrency ?? ''; const currencySymbol = currencyList?.[outputCurrency]?.symbol ?? ''; @@ -102,148 +103,150 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi // If the workspace being deleted is the active workspace, switch to the "All Workspaces" view if (activeWorkspaceID === policy?.id) { setActiveWorkspaceID(undefined); - Navigation.navigateWithSwitchPolicyID({policyID: undefined}); + Navigation.navigateWithSwitchPolicyID({ policyID: undefined }); } }, [policy?.id, policyName, activeWorkspaceID, setActiveWorkspaceID]); return ( - <WorkspacePageWithSections - headerText={translate('workspace.common.profile')} - route={route} - guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_PROFILE} - shouldShowLoading={false} - shouldUseScrollView - shouldShowOfflineIndicatorInWideScreen - shouldShowNonAdmin - icon={Illustrations.House} - > - {(hasVBA?: boolean) => ( - <View style={[styles.flex1, styles.mt3, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}> - <Section - isCentralPane - title="" - > - <Image - style={StyleSheet.flatten([styles.wAuto, styles.h68, imageStyle])} - source={illustrations.WorkspaceProfile} - resizeMode="cover" - /> - <AvatarWithImagePicker - onViewPhotoPress={() => Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy?.id ?? ''))} - source={policy?.avatar ?? ''} - size={CONST.AVATAR_SIZE.XLARGE} - avatarStyle={styles.avatarXLarge} - enablePreview - DefaultAvatar={DefaultAvatar} - type={CONST.ICON_TYPE_WORKSPACE} - fallbackIcon={Expensicons.FallbackWorkspaceAvatar} - style={[ - policy?.errorFields?.avatar ?? isSmallScreenWidth ? styles.mb1 : styles.mb3, - isSmallScreenWidth ? styles.mtn17 : styles.mtn20, - styles.alignItemsStart, - styles.sectionMenuItemTopDescription, - ]} - editIconStyle={styles.smallEditIconWorkspace} - isUsingDefaultAvatar={!policy?.avatar ?? null} - onImageSelected={(file) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file as File)} - onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')} - editorMaskImage={Expensicons.ImageCropSquareMask} - pendingAction={policy?.pendingFields?.avatar} - errors={policy?.errorFields?.avatar} - onErrorClose={() => Policy.clearAvatarErrors(policy?.id ?? '')} - previewSource={UserUtils.getFullSizeAvatar(policy?.avatar ?? '')} - headerTitle={translate('workspace.common.workspaceAvatar')} - originalFileName={policy?.originalFileName} - disabled={readOnly} - disabledStyle={styles.cursorDefault} - errorRowStyles={styles.mt3} - /> - <OfflineWithFeedback pendingAction={policy?.pendingFields?.generalSettings}> - <MenuItemWithTopDescription - title={policyName} - titleStyle={styles.workspaceTitleStyle} - description={translate('workspace.editor.nameInputLabel')} - shouldShowRightIcon={!readOnly} + <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''}> + <WorkspacePageWithSections + headerText={translate('workspace.common.profile')} + route={route} + guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_PROFILE} + shouldShowLoading={false} + shouldUseScrollView + shouldShowOfflineIndicatorInWideScreen + shouldShowNonAdmin + icon={Illustrations.House} + > + {(hasVBA?: boolean) => ( + <View style={[styles.flex1, styles.mt3, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}> + <Section + isCentralPane + title="" + > + <Image + style={StyleSheet.flatten([styles.wAuto, styles.h68, imageStyle])} + source={illustrations.WorkspaceProfile} + resizeMode="cover" + /> + <AvatarWithImagePicker + onViewPhotoPress={() => Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy?.id ?? ''))} + source={policy?.avatar ?? ''} + size={CONST.AVATAR_SIZE.XLARGE} + avatarStyle={styles.avatarXLarge} + enablePreview + DefaultAvatar={DefaultAvatar} + type={CONST.ICON_TYPE_WORKSPACE} + fallbackIcon={Expensicons.FallbackWorkspaceAvatar} + style={[ + policy?.errorFields?.avatar ?? isSmallScreenWidth ? styles.mb1 : styles.mb3, + isSmallScreenWidth ? styles.mtn17 : styles.mtn20, + styles.alignItemsStart, + styles.sectionMenuItemTopDescription, + ]} + editIconStyle={styles.smallEditIconWorkspace} + isUsingDefaultAvatar={!policy?.avatar ?? null} + onImageSelected={(file) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file as File)} + onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')} + editorMaskImage={Expensicons.ImageCropSquareMask} + pendingAction={policy?.pendingFields?.avatar} + errors={policy?.errorFields?.avatar} + onErrorClose={() => Policy.clearAvatarErrors(policy?.id ?? '')} + previewSource={UserUtils.getFullSizeAvatar(policy?.avatar ?? '')} + headerTitle={translate('workspace.common.workspaceAvatar')} + originalFileName={policy?.originalFileName} disabled={readOnly} - wrapperStyle={[styles.sectionMenuItemTopDescription, isSmallScreenWidth ? styles.mt3 : {}]} - onPress={onPressName} - shouldGreyOutWhenDisabled={false} - shouldUseDefaultCursorWhenDisabled + disabledStyle={styles.cursorDefault} + errorRowStyles={styles.mt3} /> - </OfflineWithFeedback> - {(!StringUtils.isEmptyString(policy?.description ?? '') || !readOnly) && ( - <OfflineWithFeedback - pendingAction={policy?.pendingFields?.description} - errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} - onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} - > + <OfflineWithFeedback pendingAction={policy?.pendingFields?.generalSettings}> <MenuItemWithTopDescription - title={policyDescription} - description={translate('workspace.editor.descriptionInputLabel')} + title={policyName} + titleStyle={styles.workspaceTitleStyle} + description={translate('workspace.editor.nameInputLabel')} shouldShowRightIcon={!readOnly} disabled={readOnly} - wrapperStyle={styles.sectionMenuItemTopDescription} - onPress={onPressDescription} + wrapperStyle={[styles.sectionMenuItemTopDescription, isSmallScreenWidth ? styles.mt3 : {}]} + onPress={onPressName} shouldGreyOutWhenDisabled={false} shouldUseDefaultCursorWhenDisabled - shouldRenderAsHTML /> </OfflineWithFeedback> - )} - <OfflineWithFeedback - pendingAction={policy?.pendingFields?.generalSettings} - errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} - onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} - errorRowStyles={[styles.mt2]} - > - <View> - <MenuItemWithTopDescription - title={formattedCurrency} - description={translate('workspace.editor.currencyInputLabel')} - shouldShowRightIcon={!readOnly} - disabled={hasVBA ? true : readOnly} - wrapperStyle={styles.sectionMenuItemTopDescription} - onPress={onPressCurrency} - shouldGreyOutWhenDisabled={false} - shouldUseDefaultCursorWhenDisabled - /> - <Text style={[styles.textLabel, styles.colorMuted, styles.mt1, styles.mh5, styles.sectionMenuItemTopDescription]}> - {hasVBA ? translate('workspace.editor.currencyInputDisabledText') : translate('workspace.editor.currencyInputHelpText')} - </Text> - </View> - </OfflineWithFeedback> - {!readOnly && ( - <View style={[styles.flexRow, styles.mt6, styles.mnw120]}> - <Button - accessibilityLabel={translate('common.share')} - text={translate('common.share')} - onPress={onPressShare} - medium - icon={Expensicons.QrCode} - /> - <Button - accessibilityLabel={translate('common.delete')} - text={translate('common.delete')} - style={[styles.ml2]} - onPress={() => setIsDeleteModalOpen(true)} - medium - icon={Expensicons.Trashcan} - /> - </View> - )} - </Section> - <ConfirmModal - title={translate('common.delete')} - isVisible={isDeleteModalOpen} - onConfirm={confirmDeleteAndHideModal} - onCancel={() => setIsDeleteModalOpen(false)} - prompt={translate('workspace.common.deleteConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - </View> - )} - </WorkspacePageWithSections> + {(!StringUtils.isEmptyString(policy?.description ?? '') || !readOnly) && ( + <OfflineWithFeedback + pendingAction={policy?.pendingFields?.description} + errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} + onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} + > + <MenuItemWithTopDescription + title={policyDescription} + description={translate('workspace.editor.descriptionInputLabel')} + shouldShowRightIcon={!readOnly} + disabled={readOnly} + wrapperStyle={styles.sectionMenuItemTopDescription} + onPress={onPressDescription} + shouldGreyOutWhenDisabled={false} + shouldUseDefaultCursorWhenDisabled + shouldRenderAsHTML + /> + </OfflineWithFeedback> + )} + <OfflineWithFeedback + pendingAction={policy?.pendingFields?.generalSettings} + errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} + onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} + errorRowStyles={[styles.mt2]} + > + <View> + <MenuItemWithTopDescription + title={formattedCurrency} + description={translate('workspace.editor.currencyInputLabel')} + shouldShowRightIcon={!readOnly} + disabled={hasVBA ? true : readOnly} + wrapperStyle={styles.sectionMenuItemTopDescription} + onPress={onPressCurrency} + shouldGreyOutWhenDisabled={false} + shouldUseDefaultCursorWhenDisabled + /> + <Text style={[styles.textLabel, styles.colorMuted, styles.mt1, styles.mh5, styles.sectionMenuItemTopDescription]}> + {hasVBA ? translate('workspace.editor.currencyInputDisabledText') : translate('workspace.editor.currencyInputHelpText')} + </Text> + </View> + </OfflineWithFeedback> + {!readOnly && ( + <View style={[styles.flexRow, styles.mt6, styles.mnw120]}> + <Button + accessibilityLabel={translate('common.share')} + text={translate('common.share')} + onPress={onPressShare} + medium + icon={Expensicons.QrCode} + /> + <Button + accessibilityLabel={translate('common.delete')} + text={translate('common.delete')} + style={[styles.ml2]} + onPress={() => setIsDeleteModalOpen(true)} + medium + icon={Expensicons.Trashcan} + /> + </View> + )} + </Section> + <ConfirmModal + title={translate('common.delete')} + isVisible={isDeleteModalOpen} + onConfirm={confirmDeleteAndHideModal} + onCancel={() => setIsDeleteModalOpen(false)} + prompt={translate('workspace.common.deleteConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </View> + )} + </WorkspacePageWithSections> + </AdminPolicyAccessOrNotFoundWrapper> ); } @@ -251,6 +254,6 @@ WorkspaceProfilePage.displayName = 'WorkspaceProfilePage'; export default withPolicy( withOnyx<WorkSpaceProfilePageProps, WorkSpaceProfilePageOnyxProps>({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, + currencyList: { key: ONYXKEYS.CURRENCY_LIST }, })(WorkspaceProfilePage), ); diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx index 73f24af7c325..cafe3b17ae9d 100644 --- a/src/pages/workspace/WorkspaceProfileSharePage.tsx +++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx @@ -1,13 +1,13 @@ -import React, {useRef} from 'react'; -import {View} from 'react-native'; -import type {ImageSourcePropType} from 'react-native'; +import React, { useRef } from 'react'; +import { View } from 'react-native'; +import type { ImageSourcePropType } from 'react-native'; import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png'; import ContextMenuItem from '@components/ContextMenuItem'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; -import {useSession} from '@components/OnyxProvider'; +import { useSession } from '@components/OnyxProvider'; import QRShare from '@components/QRShare'; -import type {QRShareHandle} from '@components/QRShare/types'; +import type { QRShareHandle } from '@components/QRShare/types'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useEnvironment from '@hooks/useEnvironment'; @@ -20,14 +20,15 @@ import * as Url from '@libs/Url'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import withPolicy from './withPolicy'; -import type {WithPolicyProps} from './withPolicy'; +import type { WithPolicyProps } from './withPolicy'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; -function WorkspaceProfileSharePage({policy}: WithPolicyProps) { +function WorkspaceProfileSharePage({ policy }: WithPolicyProps) { const themeStyles = useThemeStyles(); - const {translate} = useLocalize(); - const {environmentURL} = useEnvironment(); + const { translate } = useLocalize(); + const { environmentURL } = useEnvironment(); const qrCodeRef = useRef<QRShareHandle>(null); - const {isSmallScreenWidth} = useWindowDimensions(); + const { isSmallScreenWidth } = useWindowDimensions(); const session = useSession(); const policyName = policy?.name ?? ''; @@ -38,49 +39,51 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) { const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_JOIN_USER.getRoute(id, adminEmail)}`; return ( - <ScreenWrapper - testID={WorkspaceProfileSharePage.displayName} - shouldShowOfflineIndicatorInWideScreen - > - <HeaderWithBackButton - title={translate('common.share')} - onBackButtonPress={Navigation.goBack} - /> - <ScrollView style={[themeStyles.flex1, themeStyles.pt2]}> - <View style={[themeStyles.flex1, isSmallScreenWidth ? themeStyles.workspaceSectionMobile : themeStyles.workspaceSection]}> - <View style={[themeStyles.workspaceSectionMobile, themeStyles.ph9]}> - {/* + <AdminPolicyAccessOrNotFoundWrapper policyID={id}> + <ScreenWrapper + testID={WorkspaceProfileSharePage.displayName} + shouldShowOfflineIndicatorInWideScreen + > + <HeaderWithBackButton + title={translate('common.share')} + onBackButtonPress={Navigation.goBack} + /> + <ScrollView style={[themeStyles.flex1, themeStyles.pt2]}> + <View style={[themeStyles.flex1, isSmallScreenWidth ? themeStyles.workspaceSectionMobile : themeStyles.workspaceSection]}> + <View style={[themeStyles.workspaceSectionMobile, themeStyles.ph9]}> + {/* Right now QR code download button is not shown anymore This is a temporary measure because right now it's broken because of the Fabric update. We need to wait for react-native v0.74 to be released so react-native-view-shot gets fixed. Please see https://github.com/Expensify/App/issues/40110 to see if it can be re-enabled. */} - <QRShare - ref={qrCodeRef} - url={url} - title={policyName} - logo={(policy?.avatar ? policy.avatar : expensifyLogo) as ImageSourcePropType} - logoRatio={CONST.QR.DEFAULT_LOGO_SIZE_RATIO} - logoMarginRatio={CONST.QR.DEFAULT_LOGO_MARGIN_RATIO} - /> - </View> + <QRShare + ref={qrCodeRef} + url={url} + title={policyName} + logo={(policy?.avatar ? policy.avatar : expensifyLogo) as ImageSourcePropType} + logoRatio={CONST.QR.DEFAULT_LOGO_SIZE_RATIO} + logoMarginRatio={CONST.QR.DEFAULT_LOGO_MARGIN_RATIO} + /> + </View> - <View style={[themeStyles.mt3, themeStyles.ph4]}> - <ContextMenuItem - isAnonymousAction - text={translate('qrCodes.copy')} - icon={Expensicons.Copy} - successIcon={Expensicons.Checkmark} - successText={translate('qrCodes.copied')} - onPress={() => Clipboard.setString(url)} - shouldLimitWidth={false} - wrapperStyle={themeStyles.sectionMenuItemTopDescription} - /> + <View style={[themeStyles.mt3, themeStyles.ph4]}> + <ContextMenuItem + isAnonymousAction + text={translate('qrCodes.copy')} + icon={Expensicons.Copy} + successIcon={Expensicons.Checkmark} + successText={translate('qrCodes.copied')} + onPress={() => Clipboard.setString(url)} + shouldLimitWidth={false} + wrapperStyle={themeStyles.sectionMenuItemTopDescription} + /> + </View> </View> - </View> - </ScrollView> - </ScreenWrapper> + </ScrollView> + </ScreenWrapper> + </AdminPolicyAccessOrNotFoundWrapper> ); } From db6b2f702eae3801cb88b61d372425527ea13916 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 17:25:11 +0700 Subject: [PATCH 396/580] fix lint --- src/pages/workspace/WorkspaceMembersPage.tsx | 50 +++++++++---------- .../workspace/WorkspacePageWithSections.tsx | 30 +++++------ .../WorkspaceProfileCurrencyPage.tsx | 27 +++++----- .../WorkspaceProfileDescriptionPage.tsx | 28 +++++------ src/pages/workspace/WorkspaceProfilePage.tsx | 28 +++++------ .../workspace/WorkspaceProfileSharePage.tsx | 22 ++++---- 6 files changed, 92 insertions(+), 93 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 392cbfb424b5..07f89b18a028 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -1,15 +1,15 @@ -import { useIsFocused } from '@react-navigation/native'; -import type { StackScreenProps } from '@react-navigation/stack'; +import {useIsFocused} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; import lodashIsEqual from 'lodash/isEqual'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import type { TextInput } from 'react-native'; -import { InteractionManager, View } from 'react-native'; -import type { OnyxEntry } from 'react-native-onyx'; -import { withOnyx } from 'react-native-onyx'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {TextInput} from 'react-native'; +import {InteractionManager, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import Badge from '@components/Badge'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; -import type { DropdownOption, WorkspaceMemberBulkActionType } from '@components/ButtonWithDropdownMenu/types'; +import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -18,9 +18,9 @@ import MessagesRow from '@components/MessagesRow'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import TableListItem from '@components/SelectionList/TableListItem'; -import type { ListItem, SelectionListHandle } from '@components/SelectionList/types'; +import type {ListItem, SelectionListHandle} from '@components/SelectionList/types'; import Text from '@components/Text'; -import type { WithCurrentUserPersonalDetailsProps } from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -31,7 +31,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import type { WorkspacesCentralPaneNavigatorParamList } from '@libs/Navigation/types'; +import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -41,12 +41,12 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type { InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session } from '@src/types/onyx'; -import type { Errors, PendingAction } from '@src/types/onyx/OnyxCommon'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; -import type { WithPolicyAndFullscreenLoadingProps } from './withPolicyAndFullscreenLoading'; -import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session} from '@src/types/onyx'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; type WorkspaceMembersPageOnyxProps = { /** Session info for the currently logged in user. */ @@ -69,24 +69,24 @@ function invertObject(object: Record<string, string>): Record<string, string> { return inverted; } -type MemberOption = Omit<ListItem, 'accountID'> & { accountID: number }; +type MemberOption = Omit<ListItem, 'accountID'> & {accountID: number}; -function WorkspaceMembersPage({ personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true }: WorkspaceMembersPageProps) { +function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true}: WorkspaceMembersPageProps) { const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList), [policy?.employeeList]); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const [selectedEmployees, setSelectedEmployees] = useState<number[]>([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const [errors, setErrors] = useState({}); - const { isOffline } = useNetwork(); + const {isOffline} = useNetwork(); const prevIsOffline = usePrevious(isOffline); const accountIDs = useMemo(() => Object.values(policyMemberEmailsToAccountIDs ?? {}).map((accountID) => Number(accountID)), [policyMemberEmailsToAccountIDs]); const prevAccountIDs = usePrevious(accountIDs); const textInputRef = useRef<TextInput>(null); const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline; const prevPersonalDetails = usePrevious(personalDetails); - const { translate, formatPhoneNumber, preferredLocale } = useLocalize(); - const { isSmallScreenWidth } = useWindowDimensions(); + const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); const dropdownButtonRef = useRef(null); const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const isLoading = useMemo( @@ -413,7 +413,7 @@ function WorkspaceMembersPage({ personalDetails, invitedEmailsToAccountIDsDraft, <MessagesRow type="success" // eslint-disable-next-line @typescript-eslint/naming-convention - messages={{ 0: 'workspace.people.addedWithPrimary' }} + messages={{0: 'workspace.people.addedWithPrimary'}} containerStyles={[styles.pb5, styles.ph5]} onClose={() => Policy.dismissAddedWithPrimaryLoginMessages(policyID)} /> @@ -500,7 +500,7 @@ function WorkspaceMembersPage({ personalDetails, invitedEmailsToAccountIDsDraft, <ButtonWithDropdownMenu<WorkspaceMemberBulkActionType> shouldAlwaysShowDropdownMenu pressOnEnter - customText={translate('workspace.common.selected', { selectedNumber: selectedEmployees.length })} + customText={translate('workspace.common.selected', {selectedNumber: selectedEmployees.length})} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} onPress={() => null} options={getBulkActionsButtonOptions()} @@ -569,7 +569,7 @@ function WorkspaceMembersPage({ personalDetails, invitedEmailsToAccountIDsDraft, <SelectionList ref={selectionListRef} canSelectMultiple={isPolicyAdmin} - sections={[{ data, isDisabled: false }]} + sections={[{data, isDisabled: false}]} ListItem={TableListItem} disableKeyboardShortcuts={removeMembersConfirmModalVisible} headerMessage={getHeaderMessage()} @@ -597,7 +597,7 @@ export default withCurrentUserPersonalDetails( withPolicyAndFullscreenLoading( withOnyx<WorkspaceMembersPageProps, WorkspaceMembersPageOnyxProps>({ invitedEmailsToAccountIDsDraft: { - key: ({ route }) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, + key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, }, session: { key: ONYXKEYS.SESSION, diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index e3012f9053e9..adc986b2ff78 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -1,9 +1,9 @@ -import { useFocusEffect, useIsFocused } from '@react-navigation/native'; -import type { ReactNode } from 'react'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; -import { View } from 'react-native'; -import type { OnyxEntry } from 'react-native-onyx'; -import { withOnyx } from 'react-native-onyx'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; +import type {ReactNode} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -18,13 +18,13 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { Route } from '@src/ROUTES'; -import type { Policy, ReimbursementAccount, User } from '@src/types/onyx'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; +import type {Route} from '@src/ROUTES'; +import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; -import type { WithPolicyAndFullscreenLoadingProps } from './withPolicyAndFullscreenLoading'; -import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; type WorkspacePageWithSectionsOnyxProps = { /** From Onyx */ @@ -117,7 +117,7 @@ function WorkspacePageWithSections({ }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); const policyID = route.params?.policyID ?? ''; - useNetwork({ onReconnect: () => fetchData(policyID, shouldSkipVBBACall) }); + useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)}); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true; @@ -125,7 +125,7 @@ function WorkspacePageWithSections({ const isUsingECard = user?.isUsingExpensifyCard ?? false; const hasVBA = achState === BankAccount.STATE.OPEN; const content = typeof children === 'function' ? children(hasVBA, policyID, isUsingECard) : children; - const { isSmallScreenWidth } = useWindowDimensions(); + const {isSmallScreenWidth} = useWindowDimensions(); const firstRender = useRef(true); const isFocused = useIsFocused(); const prevPolicy = usePrevious(policy); @@ -156,7 +156,8 @@ function WorkspacePageWithSections({ }, [policy, shouldShowNonAdmin]); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID ?? ''} + <AdminPolicyAccessOrNotFoundWrapper + policyID={route.params.policyID ?? ''} onLinkPress={Navigation.resetToHome} shouldShow={shouldShow} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} @@ -168,7 +169,6 @@ function WorkspacePageWithSections({ testID={WorkspacePageWithSections.displayName} shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow} > - <HeaderWithBackButton title={headerText} guidesCallTaskID={guidesCallTaskID} diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index 27068583b91c..d2b5bdd080ee 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; -import type { OnyxEntry } from 'react-native-onyx'; -import { withOnyx } from 'react-native-onyx'; +import React, {useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -10,11 +10,11 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; -import type { CurrencyList } from '@src/types/onyx'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; -import type { WithPolicyAndFullscreenLoadingProps } from './withPolicyAndFullscreenLoading'; -import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import type {CurrencyList} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; type WorkspaceProfileCurrentPageOnyxProps = { /** Constant, list of available currencies */ @@ -31,8 +31,8 @@ type WorkspaceProfileCurrencyPageSectionItem = { const getDisplayText = (currencyCode: string, currencySymbol: string) => `${currencyCode} - ${currencySymbol}`; -function WorkspaceProfileCurrencyPage({ currencyList = {}, policy, isLoadingReportData = true }: WorkspaceProfileCurrentPageProps) { - const { translate } = useLocalize(); +function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingReportData = true}: WorkspaceProfileCurrentPageProps) { + const {translate} = useLocalize(); const [searchText, setSearchText] = useState(''); const trimmedText = searchText.trim().toLowerCase(); const currencyListKeys = Object.keys(currencyList ?? {}); @@ -61,7 +61,7 @@ function WorkspaceProfileCurrencyPage({ currencyList = {}, policy, isLoadingRepo }; }); - const sections = [{ data: currencyItems }]; + const sections = [{data: currencyItems}]; const headerMessage = searchText.trim() && !currencyItems.length ? translate('common.noResultsFound') : ''; @@ -71,12 +71,12 @@ function WorkspaceProfileCurrencyPage({ currencyList = {}, policy, isLoadingRepo }; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''} + <AdminPolicyAccessOrNotFoundWrapper + policyID={policy?.id ?? ''} onLinkPress={PolicyUtils.goBackFromInvalidPolicy} shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > - <ScreenWrapper includeSafeAreaPaddingBottom={false} testID={WorkspaceProfileCurrencyPage.displayName} @@ -99,7 +99,6 @@ function WorkspaceProfileCurrencyPage({ currencyList = {}, policy, isLoadingRepo /> </ScreenWrapper> </AdminPolicyAccessOrNotFoundWrapper> - ); } @@ -107,6 +106,6 @@ WorkspaceProfileCurrencyPage.displayName = 'WorkspaceProfileCurrencyPage'; export default withPolicyAndFullscreenLoading( withOnyx<WorkspaceProfileCurrentPageProps, WorkspaceProfileCurrentPageOnyxProps>({ - currencyList: { key: ONYXKEYS.CURRENCY_LIST }, + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, })(WorkspaceProfileCurrencyPage), ); diff --git a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx index 1c8de7ca2aab..1c88afd9f99c 100644 --- a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx +++ b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx @@ -1,13 +1,13 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, { useCallback, useState } from 'react'; -import { Keyboard, View } from 'react-native'; +import React, {useCallback, useState} from 'react'; +import {Keyboard, View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type { FormOnyxValues } from '@components/Form/types'; +import type {FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TextInput from '@components/TextInput'; -import type { BaseTextInputRef } from '@components/TextInput/BaseTextInput/types'; +import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -16,27 +16,27 @@ import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import withPolicy from './withPolicy'; -import type { WithPolicyProps } from './withPolicy'; import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import withPolicy from './withPolicy'; +import type {WithPolicyProps} from './withPolicy'; type Props = WithPolicyProps; const parser = new ExpensiMark(); -function WorkspaceProfileDescriptionPage({ policy }: Props) { +function WorkspaceProfileDescriptionPage({policy}: Props) { const styles = useThemeStyles(); - const { translate } = useLocalize(); + const {translate} = useLocalize(); const [description, setDescription] = useState(() => parser.htmlToMarkdown( // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing policy?.description || - parser.replace( - translate('workspace.common.welcomeNote', { - workspaceName: policy?.name ?? '', - }), - ), + parser.replace( + translate('workspace.common.welcomeNote', { + workspaceName: policy?.name ?? '', + }), + ), ), ); @@ -48,7 +48,7 @@ function WorkspaceProfileDescriptionPage({ policy }: Props) { const errors = {}; if (values.description.length > CONST.DESCRIPTION_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', { length: values.description.length, limit: CONST.DESCRIPTION_LIMIT }]); + ErrorUtils.addErrorMessage(errors, 'description', ['common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT}]); } return errors; diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index ba3cfe431496..efe803880b81 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -1,9 +1,9 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React, { useCallback, useState } from 'react'; -import type { ImageStyle, StyleProp } from 'react-native'; -import { Image, StyleSheet, View } from 'react-native'; -import type { OnyxEntry } from 'react-native-onyx'; -import { withOnyx } from 'react-native-onyx'; +import React, {useCallback, useState} from 'react'; +import type {ImageStyle, StyleProp} from 'react-native'; +import {Image, StyleSheet, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import Avatar from '@components/Avatar'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import Button from '@components/Button'; @@ -30,11 +30,11 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import withPolicy from './withPolicy'; -import type { WithPolicyProps } from './withPolicy'; +import type {WithPolicyProps} from './withPolicy'; import WorkspacePageWithSections from './WorkspacePageWithSections'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; type WorkSpaceProfilePageOnyxProps = { /** Constant, list of available currencies */ @@ -45,12 +45,12 @@ type WorkSpaceProfilePageProps = WithPolicyProps & WorkSpaceProfilePageOnyxProps const parser = new ExpensiMark(); -function WorkspaceProfilePage({ policy, currencyList = {}, route }: WorkSpaceProfilePageProps) { +function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfilePageProps) { const styles = useThemeStyles(); - const { translate } = useLocalize(); - const { isSmallScreenWidth } = useWindowDimensions(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); const illustrations = useThemeIllustrations(); - const { activeWorkspaceID, setActiveWorkspaceID } = useActiveWorkspace(); + const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); const outputCurrency = policy?.outputCurrency ?? ''; const currencySymbol = currencyList?.[outputCurrency]?.symbol ?? ''; @@ -103,7 +103,7 @@ function WorkspaceProfilePage({ policy, currencyList = {}, route }: WorkSpacePro // If the workspace being deleted is the active workspace, switch to the "All Workspaces" view if (activeWorkspaceID === policy?.id) { setActiveWorkspaceID(undefined); - Navigation.navigateWithSwitchPolicyID({ policyID: undefined }); + Navigation.navigateWithSwitchPolicyID({policyID: undefined}); } }, [policy?.id, policyName, activeWorkspaceID, setActiveWorkspaceID]); return ( @@ -254,6 +254,6 @@ WorkspaceProfilePage.displayName = 'WorkspaceProfilePage'; export default withPolicy( withOnyx<WorkSpaceProfilePageProps, WorkSpaceProfilePageOnyxProps>({ - currencyList: { key: ONYXKEYS.CURRENCY_LIST }, + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, })(WorkspaceProfilePage), ); diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx index cafe3b17ae9d..5aab39f8e019 100644 --- a/src/pages/workspace/WorkspaceProfileSharePage.tsx +++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx @@ -1,13 +1,13 @@ -import React, { useRef } from 'react'; -import { View } from 'react-native'; -import type { ImageSourcePropType } from 'react-native'; +import React, {useRef} from 'react'; +import {View} from 'react-native'; +import type {ImageSourcePropType} from 'react-native'; import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png'; import ContextMenuItem from '@components/ContextMenuItem'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; -import { useSession } from '@components/OnyxProvider'; +import {useSession} from '@components/OnyxProvider'; import QRShare from '@components/QRShare'; -import type { QRShareHandle } from '@components/QRShare/types'; +import type {QRShareHandle} from '@components/QRShare/types'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useEnvironment from '@hooks/useEnvironment'; @@ -19,16 +19,16 @@ import Navigation from '@libs/Navigation/Navigation'; import * as Url from '@libs/Url'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import withPolicy from './withPolicy'; -import type { WithPolicyProps } from './withPolicy'; import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import withPolicy from './withPolicy'; +import type {WithPolicyProps} from './withPolicy'; -function WorkspaceProfileSharePage({ policy }: WithPolicyProps) { +function WorkspaceProfileSharePage({policy}: WithPolicyProps) { const themeStyles = useThemeStyles(); - const { translate } = useLocalize(); - const { environmentURL } = useEnvironment(); + const {translate} = useLocalize(); + const {environmentURL} = useEnvironment(); const qrCodeRef = useRef<QRShareHandle>(null); - const { isSmallScreenWidth } = useWindowDimensions(); + const {isSmallScreenWidth} = useWindowDimensions(); const session = useSession(); const policyName = policy?.name ?? ''; From c6d37397d36ab75dea2ebdbae28ca745a4d97a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Thu, 25 Apr 2024 12:44:06 +0200 Subject: [PATCH 397/580] Change FULL_COMPOSER_MIN_HEIGHT variable --- src/CONST.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 7e0b3281ef74..0942a55b5f5e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -850,7 +850,7 @@ const CONST = { MAX_LINES_SMALL_SCREEN: 6, MAX_LINES_FULL: -1, // The minimum height needed to enable the full screen composer - FULL_COMPOSER_MIN_HEIGHT: 66, + FULL_COMPOSER_MIN_HEIGHT: 60, }, MODAL: { MODAL_TYPE: { From 03928e454dd950eb87653c05b8836ec44a464dd5 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Thu, 25 Apr 2024 17:49:24 +0700 Subject: [PATCH 398/580] fix: remove not admin page --- src/pages/workspace/WorkspaceInitialPage.tsx | 22 +- src/pages/workspace/WorkspaceMembersPage.tsx | 26 +- src/pages/workspace/WorkspaceProfilePage.tsx | 261 +++++++++---------- 3 files changed, 153 insertions(+), 156 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 50ae73fab500..eb41089160e3 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -5,6 +5,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import HighlightableMenuItem from '@components/HighlightableMenuItem'; @@ -36,7 +37,6 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -319,15 +319,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }, [policy]); return ( - <AdminPolicyAccessOrNotFoundWrapper - shouldShow={shouldShowNotFoundPage} - onLinkPress={Navigation.resetToHome} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - policyID={policy?.id ?? ''} + <ScreenWrapper + testID={WorkspaceInitialPage.displayName} + includeSafeAreaPaddingBottom={false} > - <ScreenWrapper - testID={WorkspaceInitialPage.displayName} - includeSafeAreaPaddingBottom={false} + <FullPageNotFoundView + onBackButtonPress={Navigation.dismissModal} + onLinkPress={Navigation.resetToHome} + shouldShow={shouldShowNotFoundPage} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > <HeaderWithBackButton title={policyName} @@ -379,8 +379,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc cancelText={translate('common.cancel')} danger /> - </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </FullPageNotFoundView> + </ScreenWrapper> ); } diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 83e017a13e30..b18522662dde 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -7,6 +7,7 @@ import {InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import Badge from '@components/Badge'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; @@ -44,7 +45,6 @@ import type SCREENS from '@src/SCREENS'; import type {InvitedEmailsToAccountIDs, PersonalDetailsList, PolicyEmployee, PolicyEmployeeList, Session} from '@src/types/onyx'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -523,17 +523,17 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, }; return ( - <AdminPolicyAccessOrNotFoundWrapper - shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || PolicyUtils.isPendingDeletePolicy(policy)} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} - policyID={policyID} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + style={[styles.defaultModalContainer]} + testID={WorkspaceMembersPage.displayName} + shouldShowOfflineIndicatorInWideScreen > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - style={[styles.defaultModalContainer]} - testID={WorkspaceMembersPage.displayName} - shouldShowOfflineIndicatorInWideScreen + <FullPageNotFoundView + shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || PolicyUtils.isPendingDeletePolicy(policy)} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + onBackButtonPress={PolicyUtils.goBackFromInvalidPolicy} + onLinkPress={PolicyUtils.goBackFromInvalidPolicy} > <HeaderWithBackButton title={translate('workspace.common.members')} @@ -586,8 +586,8 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> </View> - </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </FullPageNotFoundView> + </ScreenWrapper> ); } diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index efe803880b81..b73c6c0d7062 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -31,7 +31,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import withPolicy from './withPolicy'; import type {WithPolicyProps} from './withPolicy'; import WorkspacePageWithSections from './WorkspacePageWithSections'; @@ -107,146 +106,144 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi } }, [policy?.id, policyName, activeWorkspaceID, setActiveWorkspaceID]); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''}> - <WorkspacePageWithSections - headerText={translate('workspace.common.profile')} - route={route} - guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_PROFILE} - shouldShowLoading={false} - shouldUseScrollView - shouldShowOfflineIndicatorInWideScreen - shouldShowNonAdmin - icon={Illustrations.House} - > - {(hasVBA?: boolean) => ( - <View style={[styles.flex1, styles.mt3, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}> - <Section - isCentralPane - title="" - > - <Image - style={StyleSheet.flatten([styles.wAuto, styles.h68, imageStyle])} - source={illustrations.WorkspaceProfile} - resizeMode="cover" - /> - <AvatarWithImagePicker - onViewPhotoPress={() => Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy?.id ?? ''))} - source={policy?.avatar ?? ''} - size={CONST.AVATAR_SIZE.XLARGE} - avatarStyle={styles.avatarXLarge} - enablePreview - DefaultAvatar={DefaultAvatar} - type={CONST.ICON_TYPE_WORKSPACE} - fallbackIcon={Expensicons.FallbackWorkspaceAvatar} - style={[ - policy?.errorFields?.avatar ?? isSmallScreenWidth ? styles.mb1 : styles.mb3, - isSmallScreenWidth ? styles.mtn17 : styles.mtn20, - styles.alignItemsStart, - styles.sectionMenuItemTopDescription, - ]} - editIconStyle={styles.smallEditIconWorkspace} - isUsingDefaultAvatar={!policy?.avatar ?? null} - onImageSelected={(file) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file as File)} - onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')} - editorMaskImage={Expensicons.ImageCropSquareMask} - pendingAction={policy?.pendingFields?.avatar} - errors={policy?.errorFields?.avatar} - onErrorClose={() => Policy.clearAvatarErrors(policy?.id ?? '')} - previewSource={UserUtils.getFullSizeAvatar(policy?.avatar ?? '')} - headerTitle={translate('workspace.common.workspaceAvatar')} - originalFileName={policy?.originalFileName} + <WorkspacePageWithSections + headerText={translate('workspace.common.profile')} + route={route} + guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_PROFILE} + shouldShowLoading={false} + shouldUseScrollView + shouldShowOfflineIndicatorInWideScreen + shouldShowNonAdmin + icon={Illustrations.House} + > + {(hasVBA?: boolean) => ( + <View style={[styles.flex1, styles.mt3, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}> + <Section + isCentralPane + title="" + > + <Image + style={StyleSheet.flatten([styles.wAuto, styles.h68, imageStyle])} + source={illustrations.WorkspaceProfile} + resizeMode="cover" + /> + <AvatarWithImagePicker + onViewPhotoPress={() => Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy?.id ?? ''))} + source={policy?.avatar ?? ''} + size={CONST.AVATAR_SIZE.XLARGE} + avatarStyle={styles.avatarXLarge} + enablePreview + DefaultAvatar={DefaultAvatar} + type={CONST.ICON_TYPE_WORKSPACE} + fallbackIcon={Expensicons.FallbackWorkspaceAvatar} + style={[ + policy?.errorFields?.avatar ?? isSmallScreenWidth ? styles.mb1 : styles.mb3, + isSmallScreenWidth ? styles.mtn17 : styles.mtn20, + styles.alignItemsStart, + styles.sectionMenuItemTopDescription, + ]} + editIconStyle={styles.smallEditIconWorkspace} + isUsingDefaultAvatar={!policy?.avatar ?? null} + onImageSelected={(file) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file as File)} + onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')} + editorMaskImage={Expensicons.ImageCropSquareMask} + pendingAction={policy?.pendingFields?.avatar} + errors={policy?.errorFields?.avatar} + onErrorClose={() => Policy.clearAvatarErrors(policy?.id ?? '')} + previewSource={UserUtils.getFullSizeAvatar(policy?.avatar ?? '')} + headerTitle={translate('workspace.common.workspaceAvatar')} + originalFileName={policy?.originalFileName} + disabled={readOnly} + disabledStyle={styles.cursorDefault} + errorRowStyles={styles.mt3} + /> + <OfflineWithFeedback pendingAction={policy?.pendingFields?.generalSettings}> + <MenuItemWithTopDescription + title={policyName} + titleStyle={styles.workspaceTitleStyle} + description={translate('workspace.editor.nameInputLabel')} + shouldShowRightIcon={!readOnly} disabled={readOnly} - disabledStyle={styles.cursorDefault} - errorRowStyles={styles.mt3} + wrapperStyle={[styles.sectionMenuItemTopDescription, isSmallScreenWidth ? styles.mt3 : {}]} + onPress={onPressName} + shouldGreyOutWhenDisabled={false} + shouldUseDefaultCursorWhenDisabled /> - <OfflineWithFeedback pendingAction={policy?.pendingFields?.generalSettings}> + </OfflineWithFeedback> + {(!StringUtils.isEmptyString(policy?.description ?? '') || !readOnly) && ( + <OfflineWithFeedback + pendingAction={policy?.pendingFields?.description} + errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} + onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} + > <MenuItemWithTopDescription - title={policyName} - titleStyle={styles.workspaceTitleStyle} - description={translate('workspace.editor.nameInputLabel')} + title={policyDescription} + description={translate('workspace.editor.descriptionInputLabel')} shouldShowRightIcon={!readOnly} disabled={readOnly} - wrapperStyle={[styles.sectionMenuItemTopDescription, isSmallScreenWidth ? styles.mt3 : {}]} - onPress={onPressName} + wrapperStyle={styles.sectionMenuItemTopDescription} + onPress={onPressDescription} shouldGreyOutWhenDisabled={false} shouldUseDefaultCursorWhenDisabled + shouldRenderAsHTML /> </OfflineWithFeedback> - {(!StringUtils.isEmptyString(policy?.description ?? '') || !readOnly) && ( - <OfflineWithFeedback - pendingAction={policy?.pendingFields?.description} - errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} - onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} - > - <MenuItemWithTopDescription - title={policyDescription} - description={translate('workspace.editor.descriptionInputLabel')} - shouldShowRightIcon={!readOnly} - disabled={readOnly} - wrapperStyle={styles.sectionMenuItemTopDescription} - onPress={onPressDescription} - shouldGreyOutWhenDisabled={false} - shouldUseDefaultCursorWhenDisabled - shouldRenderAsHTML - /> - </OfflineWithFeedback> - )} - <OfflineWithFeedback - pendingAction={policy?.pendingFields?.generalSettings} - errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} - onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} - errorRowStyles={[styles.mt2]} - > - <View> - <MenuItemWithTopDescription - title={formattedCurrency} - description={translate('workspace.editor.currencyInputLabel')} - shouldShowRightIcon={!readOnly} - disabled={hasVBA ? true : readOnly} - wrapperStyle={styles.sectionMenuItemTopDescription} - onPress={onPressCurrency} - shouldGreyOutWhenDisabled={false} - shouldUseDefaultCursorWhenDisabled - /> - <Text style={[styles.textLabel, styles.colorMuted, styles.mt1, styles.mh5, styles.sectionMenuItemTopDescription]}> - {hasVBA ? translate('workspace.editor.currencyInputDisabledText') : translate('workspace.editor.currencyInputHelpText')} - </Text> - </View> - </OfflineWithFeedback> - {!readOnly && ( - <View style={[styles.flexRow, styles.mt6, styles.mnw120]}> - <Button - accessibilityLabel={translate('common.share')} - text={translate('common.share')} - onPress={onPressShare} - medium - icon={Expensicons.QrCode} - /> - <Button - accessibilityLabel={translate('common.delete')} - text={translate('common.delete')} - style={[styles.ml2]} - onPress={() => setIsDeleteModalOpen(true)} - medium - icon={Expensicons.Trashcan} - /> - </View> - )} - </Section> - <ConfirmModal - title={translate('common.delete')} - isVisible={isDeleteModalOpen} - onConfirm={confirmDeleteAndHideModal} - onCancel={() => setIsDeleteModalOpen(false)} - prompt={translate('workspace.common.deleteConfirmation')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - </View> - )} - </WorkspacePageWithSections> - </AdminPolicyAccessOrNotFoundWrapper> + )} + <OfflineWithFeedback + pendingAction={policy?.pendingFields?.generalSettings} + errors={ErrorUtils.getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} + onClose={() => Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} + errorRowStyles={[styles.mt2]} + > + <View> + <MenuItemWithTopDescription + title={formattedCurrency} + description={translate('workspace.editor.currencyInputLabel')} + shouldShowRightIcon={!readOnly} + disabled={hasVBA ? true : readOnly} + wrapperStyle={styles.sectionMenuItemTopDescription} + onPress={onPressCurrency} + shouldGreyOutWhenDisabled={false} + shouldUseDefaultCursorWhenDisabled + /> + <Text style={[styles.textLabel, styles.colorMuted, styles.mt1, styles.mh5, styles.sectionMenuItemTopDescription]}> + {hasVBA ? translate('workspace.editor.currencyInputDisabledText') : translate('workspace.editor.currencyInputHelpText')} + </Text> + </View> + </OfflineWithFeedback> + {!readOnly && ( + <View style={[styles.flexRow, styles.mt6, styles.mnw120]}> + <Button + accessibilityLabel={translate('common.share')} + text={translate('common.share')} + onPress={onPressShare} + medium + icon={Expensicons.QrCode} + /> + <Button + accessibilityLabel={translate('common.delete')} + text={translate('common.delete')} + style={[styles.ml2]} + onPress={() => setIsDeleteModalOpen(true)} + medium + icon={Expensicons.Trashcan} + /> + </View> + )} + </Section> + <ConfirmModal + title={translate('common.delete')} + isVisible={isDeleteModalOpen} + onConfirm={confirmDeleteAndHideModal} + onCancel={() => setIsDeleteModalOpen(false)} + prompt={translate('workspace.common.deleteConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + </View> + )} + </WorkspacePageWithSections> ); } From 990e3cb19e17968054dfcbe8eed33b402395e9df Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 13:25:44 +0200 Subject: [PATCH 399/580] translate --- src/languages/es.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 8d430da808a2..487b68cfacdf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3236,12 +3236,12 @@ export default { }, onboardingBottomMessage: { [CONST.INTRO_CHOICES.MANAGE_TEAM]: { - phrase1: 'Chat with your setup specialist in ', - phrase2: ' for help', + phrase1: 'Chatea con tu especialista en configuración en ', + phrase2: ' por ayuda', }, default: { - phrase1: 'Message ', - phrase2: ' for help with setup', + phrase1: 'Mensaje ', + phrase2: ' para obtener ayuda con la configuración', }, }, violations: { From 8fa54e65259629b2ebf393f2e31d490f4d3e1944 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 13:25:52 +0200 Subject: [PATCH 400/580] remove todo --- src/pages/home/report/OnboardingReportFooterMessage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index 1d02a9ebeb94..d6b86c95f141 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -16,7 +16,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy as PolicyType, Report} from '@src/types/onyx'; -// TODO: Use a proper choice type type OnboardingReportFooterMessageOnyxProps = {choice: OnyxEntry<OnboardingPurposeType>; reports: OnyxCollection<Report>; policies: OnyxCollection<PolicyType>}; type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps; From a93647a66d94ae821d14566ae848bcaed7b20213 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 13:42:48 +0200 Subject: [PATCH 401/580] fix translations --- src/languages/es.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 487b68cfacdf..b93282ad6c65 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3236,11 +3236,11 @@ export default { }, onboardingBottomMessage: { [CONST.INTRO_CHOICES.MANAGE_TEAM]: { - phrase1: 'Chatea con tu especialista en configuración en ', - phrase2: ' por ayuda', + phrase1: 'Chatea con tu especialista asignado en ', + phrase2: ' para obtener ayuda', }, default: { - phrase1: 'Mensaje ', + phrase1: 'Envía un email a ', phrase2: ' para obtener ayuda con la configuración', }, }, From 23b0ba0a104baac406a14b4615b6bd92b65589f2 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 13:50:34 +0200 Subject: [PATCH 402/580] Concierge read-only --- src/libs/actions/Report.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index c154d1ca51b5..6a2f4bf3d41e 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3171,6 +3171,7 @@ function completeOnboarding( managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, isOptimisticReport: true, + permissions: [CONST.REPORT.PERMISSIONS.READ], }, }, { @@ -3253,6 +3254,7 @@ function completeOnboarding( key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, value: { lastMentionedTime: DateUtils.getDBTime(), + permissions: [CONST.REPORT.PERMISSIONS.READ], }, }, { From ace67dcb7d83c64a27d28536f0509e4fd02c9842 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 13:50:38 +0200 Subject: [PATCH 403/580] Revert "Concierge read-only" This reverts commit 23b0ba0a104baac406a14b4615b6bd92b65589f2. --- src/libs/actions/Report.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 6a2f4bf3d41e..c154d1ca51b5 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3171,7 +3171,6 @@ function completeOnboarding( managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, isOptimisticReport: true, - permissions: [CONST.REPORT.PERMISSIONS.READ], }, }, { @@ -3254,7 +3253,6 @@ function completeOnboarding( key: `${ONYXKEYS.COLLECTION.REPORT}${targetChatReportID}`, value: { lastMentionedTime: DateUtils.getDBTime(), - permissions: [CONST.REPORT.PERMISSIONS.READ], }, }, { From 517253c38edb3d460fd0453111fe157359fe1377 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 13:58:11 +0200 Subject: [PATCH 404/580] improve OnboardingReportFooterMessage --- .../report/OnboardingReportFooterMessage.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index d6b86c95f141..7340b4f135af 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -16,7 +16,16 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy as PolicyType, Report} from '@src/types/onyx'; -type OnboardingReportFooterMessageOnyxProps = {choice: OnyxEntry<OnboardingPurposeType>; reports: OnyxCollection<Report>; policies: OnyxCollection<PolicyType>}; +type OnboardingReportFooterMessageOnyxProps = { + /** Saved onboarding purpose selected by the user */ + choice: OnyxEntry<OnboardingPurposeType>; + + /** Collection of reports */ + reports: OnyxCollection<Report>; + + /** The list of this user's policies */ + policies: OnyxCollection<PolicyType>; +}; type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps; function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingReportFooterMessageProps) { @@ -25,8 +34,8 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe const {isSmallScreenWidth} = useWindowDimensions(); const adminChatReport = useMemo(() => { - const adminsReports = reports ? Object.values(reports).filter((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS) : []; - const activePolicies = policies ? Object.values(policies).filter((policy) => PolicyUtils.shouldShowPolicy(policy, false)) : []; + const adminsReports = Object.values(reports ?? {}).filter((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS); + const activePolicies = Object.values(policies ?? {}).filter((policy) => PolicyUtils.shouldShowPolicy(policy, false)); return adminsReports.find((report) => activePolicies.find((policy) => policy?.id === report?.policyID)); }, [policies, reports]); @@ -36,27 +45,27 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe case CONST.ONBOARDING_CHOICES.MANAGE_TEAM: return ( <> - {`${translate('onboardingBottomMessage.newDotManageTeam.phrase1')}`} + {translate('onboardingBottomMessage.newDotManageTeam.phrase1')} <TextLink style={styles.label} onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminChatReport?.reportID ?? ''))} > {adminChatReport?.reportName ?? CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS} </TextLink> - {`${translate('onboardingBottomMessage.newDotManageTeam.phrase2')}`} + {translate('onboardingBottomMessage.newDotManageTeam.phrase2')} </> ); default: return ( <> - {`${translate('onboardingBottomMessage.default.phrase1')}`} + {translate('onboardingBottomMessage.default.phrase1')} <TextLink style={styles.label} onPress={() => ReportInstance.navigateToConciergeChat()} > {`${CONST?.CONCIERGE_CHAT_NAME}`} </TextLink> - {`${translate('onboardingBottomMessage.default.phrase2')}`} + {translate('onboardingBottomMessage.default.phrase2')} </> ); } From c98a79f679d1b617419d549a119004e5e1911789 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 13:59:09 +0200 Subject: [PATCH 405/580] fix check in ContextMenuActions --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 8b8799a0f106..be04db7987ed 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -525,8 +525,9 @@ const restrictedReadOnlyActions = [ 'reportActionContextMenu.deleteAction', ]; -// @ts-expect-error Not all actions have textTranslateKey -const RestrictedReadOnlyContextMenuActions: ContextMenuAction[] = ContextMenuActions.filter((action) => restrictedReadOnlyActions.includes(action.textTranslateKey)); +const RestrictedReadOnlyContextMenuActions: ContextMenuAction[] = ContextMenuActions.filter( + (action) => 'textTranslateKey' in action && restrictedReadOnlyActions.includes(action.textTranslateKey), +); export {RestrictedReadOnlyContextMenuActions}; export default ContextMenuActions; From 68bcbd2294fccc2b5992c0f94f21e8f20eeb7088 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 13:59:31 +0200 Subject: [PATCH 406/580] fix comment --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 60298d3d325c..4fb890cdd2bf 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1173,7 +1173,6 @@ function isJoinRequestInAdminRoom(report: OnyxEntry<Report>): boolean { } /** - * * Checks if report is in read-only mode. */ function isReadOnly(report: OnyxEntry<Report>): boolean { From a4e8fe538b8f70c04edc73412c7b034fd3a1237d Mon Sep 17 00:00:00 2001 From: kmichel <kmichel1030@gmail.com> Date: Thu, 25 Apr 2024 06:00:12 -0700 Subject: [PATCH 407/580] fix download error --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 3 +++ .../VideoPlayerContexts/VideoPopoverMenuContext.tsx | 12 +++++++----- src/components/VideoPlayerContexts/types.ts | 1 + 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 0138e2f870e1..f295c39f410e 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -10,6 +10,7 @@ import Hoverable from '@components/Hoverable'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; +import {useVideoPopoverMenuContext} from '@components/VideoPlayerContexts/VideoPopoverMenuContext'; import {useVolumeContext} from '@components/VideoPlayerContexts/VolumeContext'; import VideoPopoverMenu from '@components/VideoPopoverMenu'; import useNetwork from '@hooks/useNetwork'; @@ -79,6 +80,7 @@ function BaseVideoPlayer({ const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix)); const videoStateRef = useRef<AVPlaybackStatus | null>(null); const {updateVolume} = useVolumeContext(); + const {playerRef} = useVideoPopoverMenuContext(); const togglePlayCurrentVideo = useCallback(() => { videoResumeTryNumber.current = 0; @@ -93,6 +95,7 @@ function BaseVideoPlayer({ const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { setIsPopoverVisible(true); + playerRef.current = videoPlayerRef.current; if (!event || !('nativeEvent' in event)) { return; } diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx index f953ed802623..1a74616bac40 100644 --- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx +++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx @@ -1,6 +1,7 @@ -import React, {useCallback, useContext, useMemo, useState} from 'react'; +import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import type {PopoverMenuItem} from '@components/PopoverMenu'; +import type {VideoWithOnFullScreenUpdate} from '@components/VideoPlayer/types'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; @@ -18,6 +19,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState<PlaybackSpeed>(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[2]); const {isOffline} = useNetwork(); const isLocalFile = currentlyPlayingURL && CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => currentlyPlayingURL.startsWith(prefix)); + const playerRef = useRef<VideoWithOnFullScreenUpdate | null>(null); const updatePlaybackSpeed = useCallback( (speed: PlaybackSpeed) => { @@ -28,12 +30,12 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { ); const downloadAttachment = useCallback(() => { - if (currentlyPlayingURL === null) { + if (playerRef.current === null) { return; } - const sourceURI = addEncryptedAuthTokenToURL(currentlyPlayingURL); + const sourceURI = addEncryptedAuthTokenToURL(playerRef.current.props.source?.uri); fileDownload(sourceURI); - }, [currentlyPlayingURL]); + }, [playerRef]); const menuItems = useMemo(() => { const items: PopoverMenuItem[] = []; @@ -63,7 +65,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { return items; }, [currentPlaybackSpeed, downloadAttachment, translate, updatePlaybackSpeed, isOffline, isLocalFile]); - const contextValue = useMemo(() => ({menuItems, updatePlaybackSpeed}), [menuItems, updatePlaybackSpeed]); + const contextValue = useMemo(() => ({menuItems, playerRef, updatePlaybackSpeed}), [menuItems, playerRef, updatePlaybackSpeed]); return <Context.Provider value={contextValue}>{children}</Context.Provider>; } diff --git a/src/components/VideoPlayerContexts/types.ts b/src/components/VideoPlayerContexts/types.ts index ff8d9378caf7..6323c83d6d9e 100644 --- a/src/components/VideoPlayerContexts/types.ts +++ b/src/components/VideoPlayerContexts/types.ts @@ -28,6 +28,7 @@ type VolumeContext = { type VideoPopoverMenuContext = { menuItems: PopoverMenuItem[]; + playerRef: MutableRefObject<VideoWithOnFullScreenUpdate | null>; updatePlaybackSpeed: (speed: PlaybackSpeed) => void; }; From 7d05ac2e4c4ed420af245f443da22b6839c51627 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 14:31:14 +0200 Subject: [PATCH 408/580] remove payment waiting banner --- src/components/MoneyReportHeader.tsx | 14 ++------- src/components/PaymentWaitingBanner/index.tsx | 31 ------------------- .../ReportActionItem/ReportPreview.tsx | 6 ---- src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- src/languages/types.ts | 3 -- src/libs/ReportUtils.ts | 8 ----- 7 files changed, 2 insertions(+), 64 deletions(-) delete mode 100644 src/components/PaymentWaitingBanner/index.tsx diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 0f5d74f71d31..56dc6bf0075d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -22,7 +22,6 @@ import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; -import PaymentWaitingBanner from './PaymentWaitingBanner'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; import SettlementButton from './SettlementButton'; @@ -100,10 +99,6 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const shouldShowWaitingNote = ReportUtils.isInvoiceAwaitingPayment(moneyRequestReport); - - const invoicePayerName = ReportUtils.getInvoicePayerName(chatReport); - const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; @@ -113,7 +108,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy); const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount; - const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && isSmallScreenWidth) || shouldShowWaitingNote; + const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && isSmallScreenWidth); const confirmPayment = (type?: PaymentMethodType | undefined) => { if (!type) { @@ -193,7 +188,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack(undefined, false, true)} // Shows border if no buttons or next steps are showing below the header - shouldShowBorderBottom={!(shouldShowAnyButton && isSmallScreenWidth) && !(shouldShowNextStep && !isSmallScreenWidth) && !shouldShowWaitingNote} + shouldShowBorderBottom={!(shouldShowAnyButton && isSmallScreenWidth) && !(shouldShowNextStep && !isSmallScreenWidth)} shouldShowThreeDotsButton threeDotsMenuItems={threeDotsMenuItems} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} @@ -268,11 +263,6 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money <MoneyReportHeaderStatusBar nextStep={nextStep} /> </View> )} - {shouldShowWaitingNote && ( - <View style={[styles.ph5, styles.pb3]}> - <PaymentWaitingBanner payerName={invoicePayerName} /> - </View> - )} </View> {isHoldMenuVisible && requestType !== undefined && ( <ProcessMoneyReportHoldMenu diff --git a/src/components/PaymentWaitingBanner/index.tsx b/src/components/PaymentWaitingBanner/index.tsx deleted file mode 100644 index af13711f08e0..000000000000 --- a/src/components/PaymentWaitingBanner/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; - -type PaymentWaitingBannerProps = { - payerName: string; -}; - -function PaymentWaitingBanner({payerName}: PaymentWaitingBannerProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - - return ( - <View style={[styles.flexRow, styles.alignItemsCenter]}> - <Icon - src={Expensicons.Hourglass} - fill={theme.icon} - /> - - <Text style={[styles.inlineSystemMessage, styles.flexShrink1]}>{translate('iou.awaitingPayment', {payerName})}</Text> - </View> - ); -} - -export default PaymentWaitingBanner; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 4cba437b0d80..5c78e1e2604e 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -8,7 +8,6 @@ import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import PaymentWaitingBanner from '@components/PaymentWaitingBanner'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import RenderHTML from '@components/RenderHTML'; import SettlementButton from '@components/SettlementButton'; @@ -216,10 +215,6 @@ function ReportPreview({ const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID); const shouldShowRBR = !iouSettled && hasErrors; - const shouldShowWaitingNote = ReportUtils.isInvoiceAwaitingPayment(iouReport); - - const invoicePayerName = ReportUtils.getInvoicePayerName(chatReport); - /* Show subtitle if at least one of the expenses is not being smart scanned, and either: - There is more than one expense – in this case, the "X expenses, Y scanning" subtitle is shown; @@ -357,7 +352,6 @@ function ReportPreview({ isDisabled={shouldDisableSubmitButton} /> )} - {shouldShowWaitingNote && <PaymentWaitingBanner payerName={invoicePayerName} />} </View> </View> </View> diff --git a/src/languages/en.ts b/src/languages/en.ts index d9c6e7f0d5b3..2c3d58c171fe 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9,7 +9,6 @@ import type { AlreadySignedInParams, AmountEachParams, ApprovedAmountParams, - AwaitingPaymentParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, @@ -740,7 +739,6 @@ export default { set: 'set', changed: 'changed', removed: 'removed', - awaitingPayment: ({payerName}: AwaitingPaymentParams) => `Awaiting payment by ${payerName}`, chooseARate: ({unit}: ReimbursementRateParams) => `Select a workspace reimbursement rate per ${unit}`, }, notificationPreferencesPage: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 91462121f52c..e56fd67db883 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7,7 +7,6 @@ import type { AlreadySignedInParams, AmountEachParams, ApprovedAmountParams, - AwaitingPaymentParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, @@ -735,7 +734,6 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', - awaitingPayment: ({payerName}: AwaitingPaymentParams) => `A la espera de pago por ${payerName}`, chooseARate: ({unit}: ReimbursementRateParams) => `Seleccione una tasa de reembolso del espacio de trabajo por ${unit}`, }, notificationPreferencesPage: { diff --git a/src/languages/types.ts b/src/languages/types.ts index c81720f1773b..9426e343bbf0 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -136,8 +136,6 @@ type PayerSettledParams = {amount: number | string}; type WaitingOnBankAccountParams = {submitterDisplayName: string}; -type AwaitingPaymentParams = {payerName: string}; - type CanceledRequestParams = {amount: string; submitterDisplayName: string}; type AdminCanceledRequestParams = {manager: string; amount: string}; @@ -398,7 +396,6 @@ export type { ViolationsTagOutOfPolicyParams, ViolationsTaxOutOfPolicyParams, WaitingOnBankAccountParams, - AwaitingPaymentParams, WalletProgramParams, UsePlusButtonParams, WeSentYouMagicSignInLinkParams, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 12df7ba20a52..781cf13d23f6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6286,13 +6286,6 @@ function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyID: s return isChatRoom(report) && !isThread(report); } -/** - * Check if a invoice report is awaiting for payment - */ -function isInvoiceAwaitingPayment(report: OnyxEntry<Report>): boolean { - return !isSettled(report?.reportID ?? '') && isInvoiceReport(report); -} - export { addDomainToShortMention, areAllRequestsBeingSmartScanned, @@ -6519,7 +6512,6 @@ export { isInvoiceRoom, isInvoiceReport, isOpenInvoiceReport, - isInvoiceAwaitingPayment, navigateToDetailsPage, navigateToPrivateNotes, parseReportRouteParams, From 4264b50f4c90d38f42cae81ab6d1b12c5254d333 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 14:36:44 +0200 Subject: [PATCH 409/580] show invoice room in sidebar --- src/libs/ReportUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 781cf13d23f6..7589c7963a0f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4896,7 +4896,8 @@ function shouldReportBeInOptionList({ !isMoneyRequestReport(report) && !isTaskReport(report) && !isSelfDM(report) && - !isGroupChat(report)) + !isGroupChat(report) && + !isInvoiceRoom(report)) ) { return false; } From 04c20eb4b4d16811e8ff3a5cbd00c638063f26a9 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 25 Apr 2024 15:12:22 +0200 Subject: [PATCH 410/580] fix: apply requested changes --- src/hooks/useSubStep/index.ts | 13 ++++++- .../PersonalInfo/PersonalInfo.tsx | 37 +++++++++++++++---- .../PersonalInfo/substeps/AddressStep.tsx | 2 +- .../PersonalInfo/substeps/DateOfBirthStep.tsx | 2 +- .../PersonalInfo/substeps/FullNameStep.tsx | 2 +- .../PersonalInfo/substeps/PhoneNumberStep.tsx | 2 +- .../substeps/SocialSecurityNumberStep.tsx | 2 +- 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/hooks/useSubStep/index.ts b/src/hooks/useSubStep/index.ts index 2d9069abff88..94629c47b063 100644 --- a/src/hooks/useSubStep/index.ts +++ b/src/hooks/useSubStep/index.ts @@ -1,4 +1,4 @@ -import {useCallback, useRef, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; import type {SubStepProps, UseSubStep} from './types'; /** @@ -55,5 +55,14 @@ export default function useSubStep<TProps extends SubStepProps>({bodyContent, on setScreenIndex(bodyContent.length - 1); }, [bodyContent]); - return {componentToRender: bodyContent[screenIndex], isEditing: isEditing.current, screenIndex, prevScreen, nextScreen, moveTo, resetScreenIndex, goToTheLastStep}; + return { + componentToRender: bodyContent[screenIndex], + isEditing: isEditing.current, + screenIndex, + prevScreen, + nextScreen, + moveTo, + resetScreenIndex, + goToTheLastStep, + }; } diff --git a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx index 0696d0591281..c23b8bcb12fa 100644 --- a/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/EnablePayments/PersonalInfo/PersonalInfo.tsx @@ -1,6 +1,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -17,7 +18,9 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {WalletAdditionalDetailsForm} from '@src/types/form'; import INPUT_IDS from '@src/types/form/WalletAdditionalDetailsForm'; +import type {WalletAdditionalDetailsRefactor} from '@src/types/onyx/WalletAdditionalDetails'; import Address from './substeps/AddressStep'; import Confirmation from './substeps/ConfirmationStep'; import DateOfBirth from './substeps/DateOfBirthStep'; @@ -25,16 +28,23 @@ import FullName from './substeps/FullNameStep'; import PhoneNumber from './substeps/PhoneNumberStep'; import SocialSecurityNumber from './substeps/SocialSecurityNumberStep'; +type PersonalInfoPageOnyxProps = { + /** Reimbursement account from ONYX */ + walletAdditionalDetails: OnyxEntry<WalletAdditionalDetailsRefactor>; + + /** The draft values of the bank account being setup */ + walletAdditionalDetailsDraft: OnyxEntry<WalletAdditionalDetailsForm>; +}; + +type PersonalInfoPageProps = PersonalInfoPageOnyxProps; + const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.PERSONAL_INFO_STEP; const bodyContent: Array<React.ComponentType<SubStepProps>> = [FullName, DateOfBirth, Address, PhoneNumber, SocialSecurityNumber, Confirmation]; -function PersonalInfoPage() { +function PersonalInfoPage({walletAdditionalDetails, walletAdditionalDetailsDraft}: PersonalInfoPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [walletAdditionalDetails] = useOnyx(ONYXKEYS.WALLET_ADDITIONAL_DETAILS); - const [walletAdditionalDetailsDraft] = useOnyx(ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT); - const values = useMemo(() => getSubstepValues(PERSONAL_INFO_STEP_KEYS, walletAdditionalDetailsDraft, walletAdditionalDetails), [walletAdditionalDetails, walletAdditionalDetailsDraft]); const submit = () => { // TODO: uncomment after connecting steps https://github.com/Expensify/App/issues/36648 @@ -64,6 +74,7 @@ function PersonalInfoPage() { prevScreen, moveTo, screenIndex, + goToTheLastStep, } = useSubStep({ bodyContent, startFrom, @@ -72,6 +83,10 @@ function PersonalInfoPage() { const handleBackButtonPress = () => { // TODO: connect to the fist step of the wallet setup https://github.com/Expensify/App/issues/36648 + if (isEditing) { + goToTheLastStep(); + return; + } if (screenIndex === 0) { Navigation.goBack(ROUTES.SETTINGS_WALLET); Wallet.resetWalletAdditionalDetailsDraft(); @@ -82,7 +97,6 @@ function PersonalInfoPage() { return ( <ScreenWrapper - shouldShowOfflineIndicator={false} includeSafeAreaPaddingBottom={false} testID={PersonalInfoPage.displayName} > @@ -107,4 +121,13 @@ function PersonalInfoPage() { PersonalInfoPage.displayName = 'PersonalInfoPage'; -export default PersonalInfoPage; +export default withOnyx<PersonalInfoPageProps, PersonalInfoPageOnyxProps>({ + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + walletAdditionalDetails: { + key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + }, + // @ts-expect-error ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_FORM + walletAdditionalDetailsDraft: { + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT, + }, +})(PersonalInfoPage); diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx index a6a950d22adb..cbbfa0fa4d72 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx @@ -64,7 +64,7 @@ function AddressStep({onNext, isEditing}: SubStepProps) { submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} validate={validate} onSubmit={handleSubmit} - submitButtonStyles={[styles.mb0, styles.pb5]} + submitButtonStyles={[styles.mb0]} style={[styles.mh5, styles.flexGrow1]} > <View> diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx index ad59c0eb1d00..e332a977e12c 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx @@ -57,7 +57,7 @@ function DateOfBirthStep({onNext, isEditing}: SubStepProps) { validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow2, styles.justifyContentBetween]} - submitButtonStyles={[styles.pb5, styles.mb0]} + submitButtonStyles={[styles.mb0]} > <Text style={[styles.textHeadlineLineHeightXXL, styles.mb5]}>{translate('personalInfoStep.whatsYourDOB')}</Text> <InputWrapper diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx index 8842cd65838b..ef1c5803c324 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx @@ -55,7 +55,7 @@ function FullNameStep({onNext, isEditing}: SubStepProps) { validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow1]} - submitButtonStyles={[styles.pb5, styles.mb0]} + submitButtonStyles={[styles.mb0]} > <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb6]}>{translate('personalInfoStep.whatsYourLegalName')}</Text> diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx index 083f2d561570..77199ae52ea5 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx @@ -48,7 +48,7 @@ function PhoneNumberStep({onNext, isEditing}: SubStepProps) { validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow1]} - submitButtonStyles={[styles.pb5, styles.mb0]} + submitButtonStyles={[styles.mb0]} > <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.whatsYourPhoneNumber')}</Text> diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx index c992a5edf533..1e7c213d5f33 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx @@ -49,7 +49,7 @@ function SocialSecurityNumberStep({onNext, isEditing}: SubStepProps) { validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow1]} - submitButtonStyles={[styles.pb5, styles.mb0]} + submitButtonStyles={[styles.mb0]} > <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.whatsYourSSN')}</Text> From 879287c5dc5d495fae0451df183a39882f80e3de Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 25 Apr 2024 15:20:56 +0200 Subject: [PATCH 411/580] fix: minor fix --- src/hooks/useSubStep/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useSubStep/index.ts b/src/hooks/useSubStep/index.ts index 94629c47b063..43fc3061d968 100644 --- a/src/hooks/useSubStep/index.ts +++ b/src/hooks/useSubStep/index.ts @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useRef, useState} from 'react'; +import {useCallback, useRef, useState} from 'react'; import type {SubStepProps, UseSubStep} from './types'; /** From ab9b7f2979e0a40c4664a9ea84b827ccf2761dc5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 15:28:57 +0200 Subject: [PATCH 412/580] reuse Banner --- src/components/Banner.tsx | 11 ++++-- .../report/OnboardingReportFooterMessage.tsx | 34 +++++++------------ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index a46b37c986ba..94be65f2b9a2 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -17,7 +17,10 @@ import Tooltip from './Tooltip'; type BannerProps = { /** Text to display in the banner. */ - text: string; + text?: string; + + /** Content to display in the banner. */ + content?: React.ReactNode; /** Should this component render the left-aligned exclamation icon? */ shouldShowIcon?: boolean; @@ -41,7 +44,7 @@ type BannerProps = { textStyles?: StyleProp<TextStyle>; }; -function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { +function Banner({text, content, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -73,7 +76,9 @@ function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRend /> </View> )} - {shouldRenderHTML ? ( + {content && content} + + {shouldRenderHTML && text ? ( <RenderHTML html={text} /> ) : ( <Text diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index 7340b4f135af..14b679369d9e 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -1,12 +1,11 @@ import React, {useMemo} from 'react'; -import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Banner from '@components/Banner'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as PolicyUtils from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import * as ReportInstance from '@userActions/Report'; @@ -31,7 +30,6 @@ type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingReportFooterMessageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const {isSmallScreenWidth} = useWindowDimensions(); const adminChatReport = useMemo(() => { const adminsReports = Object.values(reports ?? {}).filter((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS); @@ -63,7 +61,7 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe style={styles.label} onPress={() => ReportInstance.navigateToConciergeChat()} > - {`${CONST?.CONCIERGE_CHAT_NAME}`} + {CONST?.CONCIERGE_CHAT_NAME} </TextLink> {translate('onboardingBottomMessage.default.phrase2')} </> @@ -72,23 +70,17 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe }, [adminChatReport?.reportName, adminChatReport?.reportID, choice, styles.label, translate]); return ( - <View - style={[ - styles.chatFooter, - isSmallScreenWidth ? styles.mb5 : styles.mb4, - styles.mh5, - styles.mt4, - styles.flexRow, - styles.alignItemsCenter, - styles.p4, - styles.borderRadiusComponentLarge, - styles.hoveredComponentBG, - styles.breakWord, - styles.justifyContentCenter, - ]} - > - <Text style={[styles.textSupporting, styles.label]}>{content}</Text> - </View> + <Banner + containerStyles={[styles.archivedReportFooter]} + content={ + <Text + style={[styles.label, styles.w100, styles.textAlignCenter]} + suppressHighlighting + > + {content} + </Text> + } + /> ); } From 5eb36c83359625441a7825104cd8f67ebd0aa8c9 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 15:29:58 +0200 Subject: [PATCH 413/580] add missed line --- src/pages/home/report/OnboardingReportFooterMessage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index 14b679369d9e..21352900968f 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -25,6 +25,7 @@ type OnboardingReportFooterMessageOnyxProps = { /** The list of this user's policies */ policies: OnyxCollection<PolicyType>; }; + type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps; function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingReportFooterMessageProps) { From a35ea0672b971ae3d75118c6f1c9072212678996 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Thu, 25 Apr 2024 15:31:05 +0200 Subject: [PATCH 414/580] clarify type --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index be04db7987ed..3f8b50a95501 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -517,7 +517,7 @@ const ContextMenuActions: ContextMenuAction[] = [ }, ]; -const restrictedReadOnlyActions = [ +const restrictedReadOnlyActions: TranslationPaths[] = [ 'common.download', 'reportActionContextMenu.replyInThread', 'reportActionContextMenu.editAction', From 7ff74bea5e068a5461ce95c2bfc3a6e924dc0fc6 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano <rocio@expensify.com> Date: Thu, 25 Apr 2024 16:06:07 +0200 Subject: [PATCH 415/580] Update webpack-dev-server to fix vulnerabilities --- package-lock.json | 448 ++++++++++++++++++++++++++++++++++++---------- package.json | 2 +- 2 files changed, 359 insertions(+), 91 deletions(-) diff --git a/package-lock.json b/package-lock.json index 893b171ecc55..2f4b82b56341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -242,7 +242,7 @@ "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.9.3", + "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "yaml": "^2.2.1" }, @@ -7092,9 +7092,10 @@ "license": "MIT" }, "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "dev": true, - "license": "MIT" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true }, "node_modules/@lwc/eslint-plugin-lwc": { "version": "1.7.2", @@ -12174,9 +12175,10 @@ } }, "node_modules/@types/bonjour": { - "version": "3.5.10", + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -12213,9 +12215,10 @@ } }, "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, - "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" @@ -12283,22 +12286,25 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "4.17.13", - "license": "MIT", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "dependencies": { "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", + "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "node_modules/@types/express-serve-static-core": { - "version": "4.17.30", - "license": "MIT", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.0.tgz", + "integrity": "sha512-bGyep3JqPCRry1wq+O5n7oiBgGWmeIJXPjXXCo8EK0u8duZGSYar7cGqd3ML2JUsLGeB7fmc06KYo9fLGWqPvQ==", "dependencies": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "node_modules/@types/fs-extra": { @@ -12358,6 +12364,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, "node_modules/@types/http-proxy": { "version": "1.17.9", "dev": true, @@ -12469,8 +12480,9 @@ "integrity": "sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==" }, "node_modules/@types/mime": { - "version": "3.0.1", - "license": "MIT" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/minimatch": { "version": "3.0.5", @@ -12489,6 +12501,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", @@ -12615,9 +12636,10 @@ } }, "node_modules/@types/retry": { - "version": "0.12.0", - "dev": true, - "license": "MIT" + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true }, "node_modules/@types/scheduler": { "version": "0.16.2", @@ -12627,20 +12649,32 @@ "version": "7.5.4", "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/serve-index": { - "version": "1.9.1", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, - "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.0", - "license": "MIT", + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", "dependencies": { - "@types/mime": "*", - "@types/node": "*" + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" } }, "node_modules/@types/setimmediate": { @@ -12649,9 +12683,10 @@ "license": "MIT" }, "node_modules/@types/sockjs": { - "version": "0.3.33", + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -12705,9 +12740,10 @@ } }, "node_modules/@types/ws": { - "version": "8.5.3", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -15531,21 +15567,15 @@ "license": "MIT" }, "node_modules/bonjour-service": { - "version": "1.0.13", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", "dev": true, - "license": "MIT", "dependencies": { - "array-flatten": "^2.1.2", - "dns-equal": "^1.0.0", "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, - "node_modules/bonjour-service/node_modules/array-flatten": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, "node_modules/boolbase": { "version": "1.0.0", "license": "ISC" @@ -15961,6 +15991,21 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.0.0", "license": "MIT", @@ -17874,6 +17919,22 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-browser-id": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", @@ -17889,6 +17950,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/default-browser/node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/default-gateway": { "version": "6.0.3", "dev": true, @@ -18305,15 +18378,11 @@ "license": "MIT", "optional": true }, - "node_modules/dns-equal": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/dns-packet": { - "version": "5.4.0", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, - "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -23384,6 +23453,39 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -23453,6 +23555,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "license": "MIT", @@ -26748,6 +26862,16 @@ "language-subtag-registry": "~0.3.2" } }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, "node_modules/lazy-cache": { "version": "1.0.4", "license": "MIT", @@ -28401,8 +28525,9 @@ }, "node_modules/multicast-dns": { "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, - "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -29477,15 +29602,20 @@ } }, "node_modules/p-retry": { - "version": "4.6.2", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/retry": "0.12.0", + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", "retry": "^0.13.1" }, "engines": { - "node": ">=8" + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { @@ -33050,8 +33180,9 @@ }, "node_modules/retry": { "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 4" } @@ -33116,6 +33247,18 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-node": { "version": "1.0.0", "dev": true, @@ -33298,10 +33441,12 @@ "license": "MIT" }, "node_modules/selfsigned": { - "version": "2.0.1", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, - "license": "MIT", "dependencies": { + "@types/node-forge": "^1.3.0", "node-forge": "^1" }, "engines": { @@ -35310,8 +35455,9 @@ }, "node_modules/thunky": { "version": "1.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true }, "node_modules/time-analytics-webpack-plugin": { "version": "0.1.17", @@ -37069,54 +37215,59 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.10.0", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", "dev": true, - "license": "MIT", "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", "default-gateway": "^6.0.3", "express": "^4.17.3", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", + "html-entities": "^2.4.0", "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.0.1", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { + "webpack": { + "optional": true + }, "webpack-cli": { "optional": true } @@ -37124,8 +37275,9 @@ }, "node_modules/webpack-dev-server/node_modules/ajv-keywords": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -37133,10 +37285,54 @@ "ajv": "^8.8.2" } }, + "node_modules/webpack-dev-server/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/webpack-dev-server/node_modules/colorette": { "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.1.0", @@ -37146,21 +37342,86 @@ "node": ">= 10" } }, - "node_modules/webpack-dev-server/node_modules/memfs": { - "version": "3.5.3", + "node_modules/webpack-dev-server/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, - "license": "Unlicense", "dependencies": { - "fs-monkey": "^1.0.4" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">= 4.0.0" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/webpack-dev-server/node_modules/schema-utils": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -37176,25 +37437,32 @@ } }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.3", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.2.1.tgz", + "integrity": "sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA==", "dev": true, - "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-hot-middleware": { diff --git a/package.json b/package.json index 6530f17f23b6..f9e8edff563d 100644 --- a/package.json +++ b/package.json @@ -294,7 +294,7 @@ "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.10.0", - "webpack-dev-server": "^4.9.3", + "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "yaml": "^2.2.1" }, From 0752135fd427c98bb649e947365ab3df8b354e46 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano <rocio@expensify.com> Date: Thu, 25 Apr 2024 16:20:12 +0200 Subject: [PATCH 416/580] Use same version for both libraries --- package-lock.json | 85 +++++++++++++++++++++++++++-------------------- package.json | 2 +- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f4b82b56341..e59397b33c52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -241,7 +241,7 @@ "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.10.0", + "webpack-cli": "^5.0.4", "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "yaml": "^2.2.1" @@ -13585,31 +13585,42 @@ "license": "MIT" }, "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, "node_modules/@webpack-cli/info": { - "version": "1.5.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", "dev": true, - "license": "MIT", - "dependencies": { - "envinfo": "^7.7.3" + "engines": { + "node": ">=14.15.0" }, "peerDependencies": { - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, "node_modules/@webpack-cli/serve": { - "version": "1.7.0", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, "peerDependencies": { - "webpack-cli": "4.x.x" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" }, "peerDependenciesMeta": { "webpack-dev-server": { @@ -23119,11 +23130,12 @@ } }, "node_modules/interpret": { - "version": "2.2.0", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=10.13.0" } }, "node_modules/invariant": { @@ -32783,14 +32795,15 @@ } }, "node_modules/rechoir": { - "version": "0.7.1", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", "dev": true, - "license": "MIT", "dependencies": { - "resolve": "^1.9.0" + "resolve": "^1.20.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, "node_modules/redent": { @@ -37079,43 +37092,42 @@ } }, "node_modules/webpack-cli": { - "version": "4.10.0", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", "colorette": "^2.0.14", - "commander": "^7.0.0", + "commander": "^10.0.1", "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", "webpack-merge": "^5.7.3" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=10.13.0" + "node": ">=14.15.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x" + "webpack": "5.x.x" }, "peerDependenciesMeta": { "@webpack-cli/generators": { "optional": true }, - "@webpack-cli/migrate": { - "optional": true - }, "webpack-bundle-analyzer": { "optional": true }, @@ -37130,11 +37142,12 @@ "license": "MIT" }, "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=14" } }, "node_modules/webpack-dev-middleware": { diff --git a/package.json b/package.json index f9e8edff563d..273265da9f06 100644 --- a/package.json +++ b/package.json @@ -293,7 +293,7 @@ "wait-port": "^0.2.9", "webpack": "^5.76.0", "webpack-bundle-analyzer": "^4.5.0", - "webpack-cli": "^4.10.0", + "webpack-cli": "^5.0.4", "webpack-dev-server": "^5.0.4", "webpack-merge": "^5.8.0", "yaml": "^2.2.1" From 09316a413df64fc454a2ad9c3e05ed4eecd56630 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano <rocio@expensify.com> Date: Thu, 25 Apr 2024 16:26:26 +0200 Subject: [PATCH 417/580] Fix proxySettings types --- config/webpack/webpack.dev.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 8f32a2d95c99..4a004fb61d6d 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -21,16 +21,12 @@ const getConfiguration = (environment: Environment): Promise<Configuration> => process.env.USE_WEB_PROXY === 'false' ? {} : { - proxy: { - // eslint-disable-next-line @typescript-eslint/naming-convention - '/api': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/staging': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/chat-attachments': 'http://[::1]:9000', - // eslint-disable-next-line @typescript-eslint/naming-convention - '/receipts': 'http://[::1]:9000', - }, + proxy: [ + { + context: ["/api", "/staging", "/chat-attachments", "/receipts"], + target: 'http://[::1]:9000', + }, + ], }; const baseConfig = getCommonConfiguration(environment); From 3c7b7fdf5cd5cf13e99d8e9008e5a78b199aa008 Mon Sep 17 00:00:00 2001 From: Anusha <anusharukhsar1@gmail.com> Date: Thu, 25 Apr 2024 19:27:57 +0500 Subject: [PATCH 418/580] add Scan in progress --- .../ReportActionItem/MoneyRequestView.tsx | 8 +++-- src/libs/ReportUtils.ts | 31 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 08bcc16cbbee..d76ed343baeb 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -194,7 +194,9 @@ function MoneyRequestView({ const distance = DistanceRequestUtils.convertToDistanceInMeters((transaction?.comment?.customUnit?.quantity as number) ?? 0, unit); const rateToDisplay = DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); - + const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); + const merchantTitle = isScanning ? translate('iou.receiptStatusTitle') : isEmptyMerchant ? '' : transactionMerchant; + const amountTitle = isScanning ? translate('iou.receiptStatusTitle') : formattedTransactionAmount ? formattedTransactionAmount.toString() : ''; const saveBillable = useCallback( (newBillable: boolean) => { // If the value hasn't changed, don't request to save changes on the server and just close the modal @@ -373,7 +375,7 @@ function MoneyRequestView({ {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription - title={formattedTransactionAmount ? formattedTransactionAmount.toString() : ''} + title={amountTitle} shouldShowTitleIcon={isSettled} titleIcon={Expensicons.Checkmark} description={amountDescription} @@ -406,7 +408,7 @@ function MoneyRequestView({ <OfflineWithFeedback pendingAction={getPendingFieldAction('merchant')}> <MenuItemWithTopDescription description={translate('common.merchant')} - title={isEmptyMerchant ? '' : transactionMerchant} + title={merchantTitle} interactive={canEditMerchant} shouldShowRightIcon={canEditMerchant} titleStyle={styles.flex1} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f92106d40b6e..755ddcad9639 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2623,24 +2623,9 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis const transaction = getLinkedTransaction(reportAction); - if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { - if (isEmptyObject(transaction)) { - return Localize.translateLocal('iou.trackExpense'); - } - const transactionDetails = getTransactionDetails(transaction); - return Localize.translateLocal('iou.threadTrackReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', - comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', - }); - } - if (isEmptyObject(transaction)) { // Transaction data might be empty on app's first load, if so we fallback to Expense - return Localize.translateLocal('iou.expense'); - } - - if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { - return Localize.translateLocal('iou.fieldPending'); + return ReportActionsUtils.isTrackExpenseAction(reportAction) ? Localize.translateLocal('iou.trackExpense') : Localize.translateLocal('iou.expense'); } if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { @@ -2651,9 +2636,21 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis return Localize.translateLocal('iou.receiptMissingDetails'); } + if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { + const transactionDetails = getTransactionDetails(transaction); + return Localize.translateLocal('iou.threadTrackReportName', { + formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', + comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', + }); + } + + if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { + return Localize.translateLocal('iou.fieldPending'); + } + const transactionDetails = getTransactionDetails(transaction); - return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadPaySomeoneReportName' : 'iou.threadExpenseReportName', { + return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) && !ReportActionsUtils.isTrackExpenseAction(reportAction) ? 'iou.threadPaySomeoneReportName' : 'iou.threadExpenseReportName', { formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', }); From 7ffafd7c428b0d52e31f0de715d54a7095743f8d Mon Sep 17 00:00:00 2001 From: kmichel <kmichel1030@gmail.com> Date: Thu, 25 Apr 2024 07:32:00 -0700 Subject: [PATCH 419/580] fix type error --- src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx index 1a74616bac40..cd8833039b09 100644 --- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx +++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx @@ -10,6 +10,7 @@ import CONST from '@src/CONST'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {usePlaybackContext} from './PlaybackContext'; import type {PlaybackSpeed, VideoPopoverMenuContext} from './types'; +import { AVPlaybackSourceObject } from 'expo-av'; const Context = React.createContext<VideoPopoverMenuContext | null>(null); @@ -33,7 +34,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { if (playerRef.current === null) { return; } - const sourceURI = addEncryptedAuthTokenToURL(playerRef.current.props.source?.uri); + const sourceURI = addEncryptedAuthTokenToURL((playerRef.current.props.source as AVPlaybackSourceObject).uri); fileDownload(sourceURI); }, [playerRef]); From 8ebee6ae2fe89e8745225b0e68ff6546b5f82243 Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Tue, 23 Apr 2024 17:50:15 +0200 Subject: [PATCH 420/580] Add initial version of SearchWidget --- src/ONYXKEYS.ts | 4 + src/components/Search.tsx | 116 ++++++++++-------- .../TemporaryExpenseListItem.tsx | 16 +++ src/libs/API/types.ts | 1 + src/libs/SearchUtils.ts | 24 ++++ src/libs/actions/Search.ts | 26 ++++ src/pages/Search/SearchPage.tsx | 3 +- src/stories/Search.stories.tsx | 43 ------- src/types/onyx/SearchResults.ts | 40 ++++++ src/types/onyx/index.ts | 2 + 10 files changed, 178 insertions(+), 97 deletions(-) create mode 100644 src/components/SelectionList/TemporaryExpenseListItem.tsx create mode 100644 src/libs/SearchUtils.ts create mode 100644 src/libs/actions/Search.ts delete mode 100644 src/stories/Search.stories.tsx create mode 100644 src/types/onyx/SearchResults.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5a765c93ca03..6f8e1c21284d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -356,6 +356,9 @@ const ONYXKEYS = { /** This is deprecated, but needed for a migration, so we still need to include it here so that it will be initialized in Onyx.init */ DEPRECATED_POLICY_MEMBER_LIST: 'policyMemberList_', + + // Search Page related + SEARCH: 'search_' }, /** List of Form ids */ @@ -560,6 +563,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; + [ONYXKEYS.COLLECTION.SEARCH]: OnyxTypes.SearchResults; }; type OnyxValuesMapping = { diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 38135fd2631e..37d712957fab 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,66 +1,76 @@ -import React from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; -import useLocalize from '@hooks/useLocalize'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import {PressableWithFeedback} from './Pressable'; +import {useOnyx} from 'react-native-onyx'; +import useNetwork from '@hooks/useNetwork'; +import * as SearchActions from '@libs/actions/Search'; +import * as SearchUtils from '@libs/SearchUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Text from './Text'; -import Tooltip from './Tooltip'; -type SearchProps = { - // Callback fired when component is pressed - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void; +/** + * For testing run this code in browser console to insert fake data: + * + * Onyx.set(`${ONYXKEYS.COLLECTION.SEARCH}${query}`, { + * search: { + * offset: 0, + * type: 'transaction', + * hasMoreResults: false, + * }, + * data: { + * transactions_1234: { + * receipt: {source: 'http...'}, + * hasEReceipt: false, + * created: '2024-04-11 00:00:00', + * amount: 12500, + * type: 'cash', + * reportID: '1', + * transactionThreadReportID: '2', + * transactionID: '1234', + * }, + * transactions_5555: { + * receipt: {source: 'http...'}, + * hasEReceipt: false, + * created: '2024-04-11 00:00:00', + * amount: 12500, + * type: 'cash', // not present in live data (data outside of snapshot_) + * reportID: '1', + * transactionThreadReportID: '2', + * transactionID: '5555', + * }, + * }, + * }) + */ - // Text explaining what the user can search for - placeholder?: string; +type SearchProps = { + query: string; +}; - // Text showing up in a tooltip when component is hovered - tooltip?: string; +function Search({query}: SearchProps) { + const {isOffline} = useNetwork(); + const [searchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SEARCH}${query}`); - // Styles to apply on the outer element - style?: StyleProp<ViewStyle>; + useEffect(() => { + SearchActions.search(query); + }, [query]); - /** Styles to apply to the outermost element */ - containerStyle?: StyleProp<ViewStyle>; -}; + const isLoading = !isOffline && searchResults === undefined; + const shouldShowEmptyState = isEmptyObject(searchResults); + const shouldShowResults = !isEmptyObject(searchResults); -function Search({onPress, placeholder, tooltip, style, containerStyle}: SearchProps) { - const styles = useThemeStyles(); - const theme = useTheme(); - const {translate} = useLocalize(); + const ListItem = SearchUtils.getListItem(); return ( - <View style={containerStyle}> - <Tooltip text={tooltip ?? translate('common.search')}> - <PressableWithFeedback - accessibilityLabel={tooltip ?? translate('common.search')} - role={CONST.ROLE.BUTTON} - onPress={onPress} - style={styles.searchPressable} - > - {({hovered}) => ( - <View style={[styles.searchContainer, hovered && styles.searchContainerHovered, style]}> - <Icon - src={Expensicons.MagnifyingGlass} - width={variables.iconSizeSmall} - height={variables.iconSizeSmall} - fill={theme.icon} - /> - <Text - style={styles.searchInputStyle} - numberOfLines={1} - > - {placeholder ?? translate('common.searchWithThreeDots')} - </Text> - </View> - )} - </PressableWithFeedback> - </Tooltip> + <View> + {isLoading && <Text>Loading data...</Text>} + {shouldShowEmptyState && <Text>Empty skeleton goes here</Text>} + {shouldShowResults && + SearchUtils.getTransactionsSections(searchResults?.data).map((item) => ( + <ListItem + key={item.transactionID} + item={item} + /> + ))} </View> ); } diff --git a/src/components/SelectionList/TemporaryExpenseListItem.tsx b/src/components/SelectionList/TemporaryExpenseListItem.tsx new file mode 100644 index 000000000000..dcfeb7c05542 --- /dev/null +++ b/src/components/SelectionList/TemporaryExpenseListItem.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import type {SearchTransaction} from '@src/types/onyx/SearchResults'; + +// NOTE: This is a completely temporary mock item so that something can be displayed in SearchWidget +// This should be removed and implement properly in: https://github.com/Expensify/App/issues/39877 +function ExpenseListItem({item}: {item: SearchTransaction}) { + return ( + <View> + <Text>Item: {item.transactionID}</Text> + </View> + ); +} + +export default ExpenseListItem; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 7a40400b3826..5a4000ec6cbc 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -465,6 +465,7 @@ const READ_COMMANDS = { OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage', OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage', + SEARCH: 'Search' } as const; type ReadCommand = ValueOf<typeof READ_COMMANDS>; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts new file mode 100644 index 000000000000..6d2b427feaa9 --- /dev/null +++ b/src/libs/SearchUtils.ts @@ -0,0 +1,24 @@ +import type React from 'react'; +import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; +import type * as OnyxTypes from '@src/types/onyx'; + +const searchTypeToItemMap = { + transaction: { + listItem: ExpenseListItem, + }, +}; + +const getTransactionsSections = (data: OnyxTypes.SearchResults['data']) => + Object.entries(data) + .filter(([key]) => key.startsWith('transactions_')) + .map(([, value]) => value); + +/** + * TODO: in future make this function generic and return specific item component based on type + * For now only 1 search item type exists in the app so this function is simplified + */ +function getListItem(type?: string): typeof ExpenseListItem { + return searchTypeToItemMap.transaction.listItem; +} + +export {getTransactionsSections, getListItem}; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts new file mode 100644 index 000000000000..de0114f49552 --- /dev/null +++ b/src/libs/actions/Search.ts @@ -0,0 +1,26 @@ +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import * as UserUtils from '@libs/UserUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; + +let isNetworkOffline = false; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (value) => { + isNetworkOffline = value?.isOffline ?? false; + }, +}); + +function search(query: string) { + if (isNetworkOffline) { + return; + } + + const hash = UserUtils.hashText(query, 2 ** 32); + API.read('Search', {query, hash}); +} + +export { + // eslint-disable-next-line import/prefer-default-export + search, +}; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 634dbe0572ec..7a2b905fc1bb 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,9 +1,9 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import ScreenWrapper from '@components/ScreenWrapper'; +import Search from '@components/Search'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import type SCREENS from '@src/SCREENS'; -// import EmptySearchView from './EmptySearchView'; import SearchResults from './SearchResults'; import useCustomBackHandler from './useCustomBackHandler'; @@ -15,6 +15,7 @@ function SearchPage({route}: SearchPageProps) { return ( <ScreenWrapper testID={SearchPage.displayName}> <SearchResults query={route.params.query} /> + <Search query={route.params.query} /> {/* <EmptySearchView /> */} </ScreenWrapper> ); diff --git a/src/stories/Search.stories.tsx b/src/stories/Search.stories.tsx deleted file mode 100644 index 58c3eb2561a2..000000000000 --- a/src/stories/Search.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import type {SearchProps} from '@components/Search'; -import Search from '@components/Search'; - -/** - * We use the Component Story Format for writing stories. Follow the docs here: - * - * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format - */ -const story = { - title: 'Components/Search', - component: Search, -}; - -type StoryType = typeof Template & {args?: Partial<SearchProps>}; - -function Template(args: SearchProps) { - // eslint-disable-next-line react/jsx-props-no-spreading - return <Search {...args} />; -} - -// Arguments can be passed to the component by binding -// See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Default: StoryType = Template.bind({}); -Default.args = { - onPress: () => alert('Pressed'), -}; - -const CustomPlaceholderAndTooltip: StoryType = Template.bind({}); -CustomPlaceholderAndTooltip.args = { - placeholder: 'Search for a specific thing...', - tooltip: 'Custom tooltip text', - onPress: () => alert('This component has custom placeholder text. Also custom tooltip text when hovered.'), -}; - -const CustomBackground: StoryType = Template.bind({}); -CustomBackground.args = { - onPress: () => alert('This component has custom styles applied'), - style: {backgroundColor: 'darkgreen'}, -}; - -export default story; -export {Default, CustomPlaceholderAndTooltip, CustomBackground}; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts new file mode 100644 index 000000000000..3e8fdd191ee2 --- /dev/null +++ b/src/types/onyx/SearchResults.ts @@ -0,0 +1,40 @@ +import type {Receipt} from './Transaction'; + +type SearchResultsInfo = { + offset: number; + type: string; + hasMoreResults: boolean; +}; + +type SearchTransaction = { + transactionID: string; + parentTransactionID?: string; + receipt?: Receipt; + hasEReceipt?: boolean; + created: string; + merchant: string; + modifiedCreated?: string; + modifiedMerchant?: string; + description: string; + from: {displayName: string; avatarURL: string}; + to: {displayName: string; avatarURL: string}; + amount: number; + modifiedAmount?: number; + category?: string; + tag?: string; + type: string; + hasViolation: boolean; + taxAmount?: number; + reportID: string; + transactionThreadReportID: string; // Not present in live transactions_ + action: string; +}; + +type SearchResults = { + search: SearchResultsInfo; + data: Record<string, SearchTransaction>; +}; + +export default SearchResults; + +export type {SearchTransaction}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index ea0870a7b8c6..2b4cf4c87584 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -80,6 +80,7 @@ import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; import type WorkspaceRateAndUnit from './WorkspaceRateAndUnit'; +import type SearchResults from './SearchResults' export type { Account, @@ -177,4 +178,5 @@ export type { Log, PolicyJoinMember, CapturedLogs, + SearchResults }; From 9980f663aebb3163eabaf1c81bb28d441210801a Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Wed, 24 Apr 2024 15:55:38 +0200 Subject: [PATCH 421/580] Add SearchWidget handling basic search data logic --- src/components/Search.tsx | 39 ++++++++++++++++------------ src/languages/en.ts | 6 +++++ src/languages/es.ts | 6 +++++ src/libs/SearchUtils.ts | 3 +-- src/pages/Search/EmptySearchView.tsx | 19 ++++++++++++-- src/pages/Search/SearchPage.tsx | 3 --- src/pages/Search/SearchResults.tsx | 17 ------------ 7 files changed, 52 insertions(+), 41 deletions(-) delete mode 100644 src/pages/Search/SearchResults.tsx diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 37d712957fab..d656e9f02174 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,17 +1,17 @@ import React, {useEffect} from 'react'; -import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import useNetwork from '@hooks/useNetwork'; import * as SearchActions from '@libs/actions/Search'; import * as SearchUtils from '@libs/SearchUtils'; +import EmptySearchView from '@pages/Search/EmptySearchView'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import Text from './Text'; +import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; /** - * For testing run this code in browser console to insert fake data: - * - * Onyx.set(`${ONYXKEYS.COLLECTION.SEARCH}${query}`, { + * For testing purposes run this code in browser console to insert fake data: + * query is the param from URL, by default it will be "all" + * Onyx.set(`search_${query}`, { * search: { * offset: 0, * type: 'transaction', @@ -58,20 +58,25 @@ function Search({query}: SearchProps) { const shouldShowEmptyState = isEmptyObject(searchResults); const shouldShowResults = !isEmptyObject(searchResults); - const ListItem = SearchUtils.getListItem(); + let resultsToDisplay = null; + + if (shouldShowResults) { + const ListItem = SearchUtils.getListItem(); + + resultsToDisplay = SearchUtils.getTransactionsSections(searchResults.data).map((item) => ( + <ListItem + key={item.transactionID} + item={item} + /> + )); + } return ( - <View> - {isLoading && <Text>Loading data...</Text>} - {shouldShowEmptyState && <Text>Empty skeleton goes here</Text>} - {shouldShowResults && - SearchUtils.getTransactionsSections(searchResults?.data).map((item) => ( - <ListItem - key={item.transactionID} - item={item} - /> - ))} - </View> + <> + {isLoading && <TableListItemSkeleton shouldAnimate />} + {shouldShowEmptyState && <EmptySearchView />} + {shouldShowResults && resultsToDisplay} + </> ); } diff --git a/src/languages/en.ts b/src/languages/en.ts index 9738be63911a..4bf45a608b48 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2452,6 +2452,12 @@ export default { }, search: { resultsAreLimited: 'Search results are limited.', + searchResults: { + emptyResults:{ + title: 'Nothing to show', + subtitle: 'Try creating something using the green + button.', + } + } }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/languages/es.ts b/src/languages/es.ts index 94b1ab030fb4..406313631049 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2483,6 +2483,12 @@ export default { }, search: { resultsAreLimited: 'Los resultados de búsqueda están limitados.', + searchResults: { + emptyResults: { + title: 'No hay nada que ver aquí', + subtitle: 'Por favor intenta crear algo usando el botón verde.', + }, + }, }, genericErrorPage: { title: '¡Oh-oh, algo salió mal!', diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 6d2b427feaa9..3d9ebeea5d50 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,4 +1,3 @@ -import type React from 'react'; import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; import type * as OnyxTypes from '@src/types/onyx'; @@ -17,7 +16,7 @@ const getTransactionsSections = (data: OnyxTypes.SearchResults['data']) => * TODO: in future make this function generic and return specific item component based on type * For now only 1 search item type exists in the app so this function is simplified */ -function getListItem(type?: string): typeof ExpenseListItem { +function getListItem(): typeof ExpenseListItem { return searchTypeToItemMap.transaction.listItem; } diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 7109b1faa1d4..e4e7d6f8eb1b 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -1,8 +1,23 @@ import React from 'react'; -import TableListItemSkeleton from '@components/Skeletons/TableListItemSkeleton'; +import {View} from 'react-native'; +import * as Illustrations from '@components/Icon/Illustrations'; +import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; function EmptySearchView() { - return <TableListItemSkeleton shouldAnimate />; + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + <View style={[styles.m4]}> + <WorkspaceEmptyStateSection + icon={Illustrations.EmptyStateExpenses} + title={translate('search.searchResults.emptyResults.title')} + subtitle={translate('search.searchResults.emptyResults.subtitle')} + /> + </View> + ); } EmptySearchView.displayName = 'EmptySearchView'; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 7a2b905fc1bb..8facbbba9446 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -4,7 +4,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import type SCREENS from '@src/SCREENS'; -import SearchResults from './SearchResults'; import useCustomBackHandler from './useCustomBackHandler'; type SearchPageProps = StackScreenProps<CentralPaneNavigatorParamList, typeof SCREENS.SEARCH.CENTRAL_PANE>; @@ -14,9 +13,7 @@ function SearchPage({route}: SearchPageProps) { return ( <ScreenWrapper testID={SearchPage.displayName}> - <SearchResults query={route.params.query} /> <Search query={route.params.query} /> - {/* <EmptySearchView /> */} </ScreenWrapper> ); } diff --git a/src/pages/Search/SearchResults.tsx b/src/pages/Search/SearchResults.tsx deleted file mode 100644 index 261484bc13ec..000000000000 --- a/src/pages/Search/SearchResults.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import Text from '@components/Text'; -import useThemeStyles from '@hooks/useThemeStyles'; - -type SearchResultsProps = { - query: string; -}; - -function SearchResults({query}: SearchResultsProps) { - const styles = useThemeStyles(); - - return <Text style={styles.textHeadlineH1}>Search results for: |{query}| filter</Text>; -} - -SearchResults.displayName = 'SearchResults'; - -export default SearchResults; From 67a43b1930d606cc9604ecd257e028460034003c Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Thu, 25 Apr 2024 16:44:24 +0200 Subject: [PATCH 422/580] fix: remove unnecessary style --- src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx | 1 - .../EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx | 1 - src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx | 1 - .../EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx | 1 - .../PersonalInfo/substeps/SocialSecurityNumberStep.tsx | 1 - 5 files changed, 5 deletions(-) diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx index cbbfa0fa4d72..ad490fa2d7c9 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/AddressStep.tsx @@ -64,7 +64,6 @@ function AddressStep({onNext, isEditing}: SubStepProps) { submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} validate={validate} onSubmit={handleSubmit} - submitButtonStyles={[styles.mb0]} style={[styles.mh5, styles.flexGrow1]} > <View> diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx index e332a977e12c..cfd0f4c5e3f7 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/DateOfBirthStep.tsx @@ -57,7 +57,6 @@ function DateOfBirthStep({onNext, isEditing}: SubStepProps) { validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow2, styles.justifyContentBetween]} - submitButtonStyles={[styles.mb0]} > <Text style={[styles.textHeadlineLineHeightXXL, styles.mb5]}>{translate('personalInfoStep.whatsYourDOB')}</Text> <InputWrapper diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx index ef1c5803c324..f63d5ef84879 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/FullNameStep.tsx @@ -55,7 +55,6 @@ function FullNameStep({onNext, isEditing}: SubStepProps) { validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow1]} - submitButtonStyles={[styles.mb0]} > <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb6]}>{translate('personalInfoStep.whatsYourLegalName')}</Text> diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx index 77199ae52ea5..86b6a40948fc 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/PhoneNumberStep.tsx @@ -48,7 +48,6 @@ function PhoneNumberStep({onNext, isEditing}: SubStepProps) { validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow1]} - submitButtonStyles={[styles.mb0]} > <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.whatsYourPhoneNumber')}</Text> diff --git a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx index 1e7c213d5f33..4c366c5a1873 100644 --- a/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx +++ b/src/pages/EnablePayments/PersonalInfo/substeps/SocialSecurityNumberStep.tsx @@ -49,7 +49,6 @@ function SocialSecurityNumberStep({onNext, isEditing}: SubStepProps) { validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow1]} - submitButtonStyles={[styles.mb0]} > <View> <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('personalInfoStep.whatsYourSSN')}</Text> From 5c8de390659786a3e96d86d55ca67a819adebe25 Mon Sep 17 00:00:00 2001 From: Anusha <anusharukhsar1@gmail.com> Date: Thu, 25 Apr 2024 19:45:37 +0500 Subject: [PATCH 423/580] run prettier --- src/libs/ReportUtils.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 755ddcad9639..16f11ef02857 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2624,7 +2624,7 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis const transaction = getLinkedTransaction(reportAction); if (isEmptyObject(transaction)) { - // Transaction data might be empty on app's first load, if so we fallback to Expense + // Transaction data might be empty on app's first load, if so we fallback to Expense/Track Expense return ReportActionsUtils.isTrackExpenseAction(reportAction) ? Localize.translateLocal('iou.trackExpense') : Localize.translateLocal('iou.expense'); } @@ -2650,10 +2650,13 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis const transactionDetails = getTransactionDetails(transaction); - return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) && !ReportActionsUtils.isTrackExpenseAction(reportAction) ? 'iou.threadPaySomeoneReportName' : 'iou.threadExpenseReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', - comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', - }); + return Localize.translateLocal( + ReportActionsUtils.isSentMoneyReportAction(reportAction) && !ReportActionsUtils.isTrackExpenseAction(reportAction) ? 'iou.threadPaySomeoneReportName' : 'iou.threadExpenseReportName', + { + formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', + comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', + }, + ); } /** From c9cec18f4ce7753f42a05b058dcd139abbb7728c Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Thu, 25 Apr 2024 10:52:13 -0400 Subject: [PATCH 424/580] fix: use ToggleSettingOptionRow and use correct subtitle --- .../WorkspaceCategoriesSettingsPage.tsx | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 4bd715ed13c8..02891ab87c90 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -2,10 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; -import Switch from '@components/Switch'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceRequiresCategory} from '@libs/actions/Policy'; @@ -15,6 +12,7 @@ import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabl import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -46,28 +44,16 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet > <HeaderWithBackButton title={translate('common.settings')} /> <View style={styles.flexGrow1}> - <OfflineWithFeedback - errors={policy?.errorFields?.requiresCategory} + <ToggleSettingOptionRow + title={translate('workspace.categories.requiresCategory')} + subtitle={isConnectedToAccounting ? `${translate('workspace.categories.needCategoryForExportToIntegration')} ${translate('workspace.accounting.qbo')}` : ''} + isActive={policy?.requiresCategory ?? false} + onToggle={updateWorkspaceRequiresCategory} pendingAction={policy?.pendingFields?.requiresCategory} - errorRowStyles={styles.mh5} - > - <View style={[styles.mt2, styles.mh4]}> - <View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Text style={[styles.textNormal, styles.colorMuted, styles.flexShrink1, styles.mr2]}>{translate('workspace.categories.requiresCategory')}</Text> - <Switch - isOn={policy?.requiresCategory ?? false} - accessibilityLabel={translate('workspace.categories.requiresCategory')} - onToggle={updateWorkspaceRequiresCategory} - disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} - /> - </View> - </View> - {isConnectedToAccounting && ( - <Text style={[styles.textNormal, styles.colorMuted]}>{`${translate('workspace.categories.importedFromAccountingSoftware')}${translate( - 'workspace.accounting.qbo', - )}`}</Text> - )} - </OfflineWithFeedback> + disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} + wrapperStyle={[styles.mt2, styles.mh4]} + errors={policy?.errorFields?.requiresCategory ?? undefined} + /> </View> </ScreenWrapper> </FeatureEnabledAccessOrNotFoundWrapper> From 80ecabce34e75dbfee6e3ad4db31460831718756 Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Thu, 25 Apr 2024 21:57:00 +0700 Subject: [PATCH 425/580] Save button does not move above the keyboard when keyboard is up --- src/components/Modal/BaseModal.tsx | 36 +++++++++++-------- src/components/Modal/types.ts | 3 ++ .../TextPicker/TextSelectorModal.tsx | 4 +++ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 2448f65a75f4..38701389f78b 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -46,6 +46,7 @@ function BaseModal( onBackdropPress, shouldEnableNewFocusManagement = false, restoreFocusType, + shouldUseModalPaddingStyle = true, }: BaseModalProps, ref: React.ForwardedRef<View>, ) { @@ -173,21 +174,26 @@ function BaseModal( paddingRight: safeAreaPaddingRight, } = StyleUtils.getSafeAreaPadding(safeAreaInsets); - const modalPaddingStyles = StyleUtils.getModalPaddingStyles({ - safeAreaPaddingTop, - safeAreaPaddingBottom, - safeAreaPaddingLeft, - safeAreaPaddingRight, - shouldAddBottomSafeAreaMargin, - shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaPadding: !keyboardStateContextValue?.isKeyboardShown && shouldAddBottomSafeAreaPadding, - shouldAddTopSafeAreaPadding, - modalContainerStyleMarginTop: modalContainerStyle.marginTop, - modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, - modalContainerStylePaddingTop: modalContainerStyle.paddingTop, - modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, - insets: safeAreaInsets, - }); + const modalPaddingStyles = shouldUseModalPaddingStyle + ? StyleUtils.getModalPaddingStyles({ + safeAreaPaddingTop, + safeAreaPaddingBottom, + safeAreaPaddingLeft, + safeAreaPaddingRight, + shouldAddBottomSafeAreaMargin, + shouldAddTopSafeAreaMargin, + shouldAddBottomSafeAreaPadding: !keyboardStateContextValue?.isKeyboardShown && shouldAddBottomSafeAreaPadding, + shouldAddTopSafeAreaPadding, + modalContainerStyleMarginTop: modalContainerStyle.marginTop, + modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, + modalContainerStylePaddingTop: modalContainerStyle.paddingTop, + modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, + insets: safeAreaInsets, + }) + : { + paddingLeft: safeAreaPaddingLeft ?? 0, + paddingRight: safeAreaPaddingRight ?? 0, + }; return ( // this is a workaround for modal not being visible on the new arch in some cases diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 6111987e9c8d..bb17f2b4846f 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -77,6 +77,9 @@ type BaseModalProps = Partial<ModalProps> & { /** How to re-focus after the modal is dismissed */ restoreFocusType?: ValueOf<typeof CONST.MODAL.RESTORE_FOCUS_TYPE>; + + /** Should we apply padding style in modal itself. If this value is false, we will handle it in ScreenWrapper */ + shouldUseModalPaddingStyle?: boolean; }; export default BaseModalProps; diff --git a/src/components/TextPicker/TextSelectorModal.tsx b/src/components/TextPicker/TextSelectorModal.tsx index d7d621ff46cf..eaffc68f032c 100644 --- a/src/components/TextPicker/TextSelectorModal.tsx +++ b/src/components/TextPicker/TextSelectorModal.tsx @@ -7,6 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import type {TextSelectorModalProps} from './types'; @@ -17,6 +18,8 @@ function TextSelectorModal({value, description = '', onValueSelected, isVisible, const [currentValue, setValue] = useState(value); + const {paddingTop, paddingBottom} = useStyledSafeAreaInsets(); + return ( <Modal type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED} @@ -31,6 +34,7 @@ function TextSelectorModal({value, description = '', onValueSelected, isVisible, includeSafeAreaPaddingBottom={false} testID={TextSelectorModal.displayName} shouldEnableMaxHeight + style={{paddingTop, paddingBottom}} > <HeaderWithBackButton title={description} From bb4195b29f1217baa617b0c4a49779e5b71b8a40 Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Thu, 25 Apr 2024 15:22:49 +0200 Subject: [PATCH 426/580] Improve SearchWidget layout and behavior --- src/components/Search.tsx | 101 +++++++++--------- .../TemporaryExpenseListItem.tsx | 4 +- src/libs/SearchUtils.ts | 3 +- src/libs/actions/Search.ts | 3 +- src/pages/Search/EmptySearchView.tsx | 15 +-- src/pages/Search/SearchFilters.tsx | 18 ++-- src/pages/Search/SearchFiltersNarrow.tsx | 11 +- src/pages/Search/SearchPageBottomTab.tsx | 15 ++- 8 files changed, 83 insertions(+), 87 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index d656e9f02174..611c1498e264 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -6,41 +6,40 @@ import * as SearchUtils from '@libs/SearchUtils'; import EmptySearchView from '@pages/Search/EmptySearchView'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; -/** - * For testing purposes run this code in browser console to insert fake data: - * query is the param from URL, by default it will be "all" - * Onyx.set(`search_${query}`, { - * search: { - * offset: 0, - * type: 'transaction', - * hasMoreResults: false, - * }, - * data: { - * transactions_1234: { - * receipt: {source: 'http...'}, - * hasEReceipt: false, - * created: '2024-04-11 00:00:00', - * amount: 12500, - * type: 'cash', - * reportID: '1', - * transactionThreadReportID: '2', - * transactionID: '1234', - * }, - * transactions_5555: { - * receipt: {source: 'http...'}, - * hasEReceipt: false, - * created: '2024-04-11 00:00:00', - * amount: 12500, - * type: 'cash', // not present in live data (data outside of snapshot_) - * reportID: '1', - * transactionThreadReportID: '2', - * transactionID: '5555', - * }, - * }, - * }) - */ +// For testing purposes run this code in browser console to insert fake data: +// query is the param from URL, by default it will be "all" +// Onyx.set(`search_${query}`, { +// search: { +// offset: 0, +// type: 'transaction', +// hasMoreResults: false, +// }, +// data: { +// transactions_1234: { +// receipt: {source: 'http...'}, +// hasEReceipt: false, +// created: '2024-04-11 00:00:00', +// amount: 12500, +// type: 'cash', +// reportID: '1', +// transactionThreadReportID: '2', +// transactionID: '1234', +// }, +// transactions_5555: { +// receipt: {source: 'http...'}, +// hasEReceipt: false, +// created: '2024-04-11 00:00:00', +// amount: 12500, +// type: 'cash', // not present in live data (data outside of snapshot_) +// reportID: '1', +// transactionThreadReportID: '2', +// transactionID: '5555', +// }, +// }, +// }) type SearchProps = { query: string; @@ -48,36 +47,32 @@ type SearchProps = { function Search({query}: SearchProps) { const {isOffline} = useNetwork(); - const [searchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SEARCH}${query}`); + const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SEARCH}${query}`); useEffect(() => { SearchActions.search(query); }, [query]); - const isLoading = !isOffline && searchResults === undefined; + const isLoading = !isOffline && isLoadingOnyxValue(searchResultsMeta); const shouldShowEmptyState = isEmptyObject(searchResults); - const shouldShowResults = !isEmptyObject(searchResults); - let resultsToDisplay = null; - - if (shouldShowResults) { - const ListItem = SearchUtils.getListItem(); + if (isLoading) { + return <TableListItemSkeleton shouldAnimate />; + } - resultsToDisplay = SearchUtils.getTransactionsSections(searchResults.data).map((item) => ( - <ListItem - key={item.transactionID} - item={item} - /> - )); + if (shouldShowEmptyState) { + return <EmptySearchView />; } - return ( - <> - {isLoading && <TableListItemSkeleton shouldAnimate />} - {shouldShowEmptyState && <EmptySearchView />} - {shouldShowResults && resultsToDisplay} - </> - ); + const ListItem = SearchUtils.getListItem(); + + // This will be updated with the proper List component in another PR + return SearchUtils.getTransactionsSections(searchResults.data).map((item) => ( + <ListItem + key={item.transactionID} + item={item} + /> + )); } Search.displayName = 'Search'; diff --git a/src/components/SelectionList/TemporaryExpenseListItem.tsx b/src/components/SelectionList/TemporaryExpenseListItem.tsx index dcfeb7c05542..011a6d73ac4f 100644 --- a/src/components/SelectionList/TemporaryExpenseListItem.tsx +++ b/src/components/SelectionList/TemporaryExpenseListItem.tsx @@ -1,13 +1,15 @@ import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; // NOTE: This is a completely temporary mock item so that something can be displayed in SearchWidget // This should be removed and implement properly in: https://github.com/Expensify/App/issues/39877 function ExpenseListItem({item}: {item: SearchTransaction}) { + const styles = useThemeStyles(); return ( - <View> + <View style={[styles.pt8]}> <Text>Item: {item.transactionID}</Text> </View> ); diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 3d9ebeea5d50..ccced12a0bc4 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,5 +1,6 @@ import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; import type * as OnyxTypes from '@src/types/onyx'; +import type {SearchTransaction} from '@src/types/onyx/SearchResults'; const searchTypeToItemMap = { transaction: { @@ -7,7 +8,7 @@ const searchTypeToItemMap = { }, }; -const getTransactionsSections = (data: OnyxTypes.SearchResults['data']) => +const getTransactionsSections = (data: OnyxTypes.SearchResults['data']): SearchTransaction[] => Object.entries(data) .filter(([key]) => key.startsWith('transactions_')) .map(([, value]) => value); diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index de0114f49552..0165c5dccb34 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import * as UserUtils from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import {READ_COMMANDS} from '@libs/API/types'; let isNetworkOffline = false; Onyx.connect({ @@ -17,7 +18,7 @@ function search(query: string) { } const hash = UserUtils.hashText(query, 2 ** 32); - API.read('Search', {query, hash}); + API.read(READ_COMMANDS.SEARCH, {query, hash}); } export { diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index e4e7d6f8eb1b..bfeb46f06298 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -1,22 +1,17 @@ import React from 'react'; -import {View} from 'react-native'; import * as Illustrations from '@components/Icon/Illustrations'; import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; function EmptySearchView() { - const styles = useThemeStyles(); const {translate} = useLocalize(); return ( - <View style={[styles.m4]}> - <WorkspaceEmptyStateSection - icon={Illustrations.EmptyStateExpenses} - title={translate('search.searchResults.emptyResults.title')} - subtitle={translate('search.searchResults.emptyResults.subtitle')} - /> - </View> + <WorkspaceEmptyStateSection + icon={Illustrations.EmptyStateExpenses} + title={translate('search.searchResults.emptyResults.title')} + subtitle={translate('search.searchResults.emptyResults.subtitle')} + /> ); } diff --git a/src/pages/Search/SearchFilters.tsx b/src/pages/Search/SearchFilters.tsx index 46dfd05416ec..0ce2f958043c 100644 --- a/src/pages/Search/SearchFilters.tsx +++ b/src/pages/Search/SearchFilters.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; import MenuItem from '@components/MenuItem'; -import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -14,17 +13,20 @@ import ROUTES from '@src/ROUTES'; import type IconAsset from '@src/types/utils/IconAsset'; import SearchFiltersNarrow from './SearchFiltersNarrow'; +type SearchFiltersProps = { + query: string; +}; + type SearchMenuFilterItem = { title: string; icon: IconAsset; route: Route; }; -function SearchFilters() { +function SearchFilters({query}: SearchFiltersProps) { const styles = useThemeStyles(); - const {singleExecution} = useSingleExecution(); - const activeRoute = useActiveRoute(); const {isSmallScreenWidth} = useWindowDimensions(); + const {singleExecution} = useSingleExecution(); const {translate} = useLocalize(); const filterItems: SearchMenuFilterItem[] = [ @@ -35,15 +37,11 @@ function SearchFilters() { }, ]; - const currentQuery = activeRoute?.params && 'query' in activeRoute.params ? activeRoute?.params?.query : ''; - if (isSmallScreenWidth) { - const activeItemLabel = String(currentQuery); - return ( <SearchFiltersNarrow filterItems={filterItems} - activeItemLabel={activeItemLabel} + activeItemLabel={String(query)} /> ); } @@ -51,7 +49,7 @@ function SearchFilters() { return ( <View style={[styles.pb4, styles.mh3, styles.mt3]}> {filterItems.map((item) => { - const isActive = item.title.toLowerCase() === currentQuery; + const isActive = item.title.toLowerCase() === query; const onPress = singleExecution(() => Navigation.navigate(item.route)); return ( diff --git a/src/pages/Search/SearchFiltersNarrow.tsx b/src/pages/Search/SearchFiltersNarrow.tsx index 935c911fafff..01e750a9a2ce 100644 --- a/src/pages/Search/SearchFiltersNarrow.tsx +++ b/src/pages/Search/SearchFiltersNarrow.tsx @@ -1,5 +1,5 @@ import React, {useRef, useState} from 'react'; -import {Animated, StyleSheet, View} from 'react-native'; +import {Animated, View} from 'react-native'; import Icon from '@components/Icon'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; @@ -42,17 +42,16 @@ function SearchFiltersNarrow({filterItems, activeItemLabel}: SearchFiltersNarrow })); return ( - <> + <View style={[styles.pb4]}> <PressableWithFeedback accessible accessibilityLabel={popoverMenuItems[activeItemIndex]?.text ?? ''} - style={[styles.tabSelectorButton]} - wrapperStyle={[styles.flex1]} ref={buttonRef} + style={[styles.tabSelectorButton]} onPress={openMenu} > {({hovered}) => ( - <Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, styles.tabBackground(hovered, true, theme.border), styles.mh3]}> + <Animated.View style={[styles.tabSelectorButton, styles.tabBackground(hovered, true, theme.border), styles.w100, styles.mh3]}> <View style={[styles.flexRow]}> <Icon src={popoverMenuItems[activeItemIndex]?.icon ?? Expensicons.All} @@ -75,7 +74,7 @@ function SearchFiltersNarrow({filterItems, activeItemLabel}: SearchFiltersNarrow onItemSelected={closeMenu} anchorRef={buttonRef} /> - </> + </View> ); } diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 4615b805d570..169485f44001 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -1,16 +1,22 @@ import React from 'react'; import ScreenWrapper from '@components/ScreenWrapper'; +import Search from '@components/Search'; +import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; import SearchFilters from './SearchFilters'; -// import EmptySearchView from './EmptySearchView'; - function SearchPageBottomTab() { const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + const activeRoute = useActiveRoute(); const styles = useThemeStyles(); + const currentQuery = activeRoute?.params && 'query' in activeRoute.params ? activeRoute?.params?.query : ''; + const query = String(currentQuery); + return ( <ScreenWrapper testID={SearchPageBottomTab.displayName} @@ -20,9 +26,8 @@ function SearchPageBottomTab() { breadcrumbLabel={translate('common.search')} shouldDisplaySearch={false} /> - <SearchFilters /> - {/* <EmptySearchView /> */} - {/* Search results list goes here */} + <SearchFilters query={query} /> + {isSmallScreenWidth && <Search query={query} />} </ScreenWrapper> ); } From 1210719d4bae92ded5a4733eaf2b991ad6d0f266 Mon Sep 17 00:00:00 2001 From: tienifr <christianwen18@gmail.com> Date: Thu, 25 Apr 2024 22:03:15 +0700 Subject: [PATCH 427/580] disable shouldUseModalPaddingStyle --- src/components/TextPicker/TextSelectorModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TextPicker/TextSelectorModal.tsx b/src/components/TextPicker/TextSelectorModal.tsx index eaffc68f032c..0fe7580746c5 100644 --- a/src/components/TextPicker/TextSelectorModal.tsx +++ b/src/components/TextPicker/TextSelectorModal.tsx @@ -28,6 +28,7 @@ function TextSelectorModal({value, description = '', onValueSelected, isVisible, onModalHide={onClose} hideModalContentWhileAnimating useNativeDriver + shouldUseModalPaddingStyle={false} > <ScreenWrapper includePaddingTop={false} From 4d4e25577d88105cf3734496db7d1266f13b6240 Mon Sep 17 00:00:00 2001 From: Anusha <anusharukhsar1@gmail.com> Date: Thu, 25 Apr 2024 20:16:40 +0500 Subject: [PATCH 428/580] Fix lint error --- src/components/ReportActionItem/MoneyRequestView.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d76ed343baeb..247ed91a961c 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -195,8 +195,10 @@ function MoneyRequestView({ const rateToDisplay = DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); - const merchantTitle = isScanning ? translate('iou.receiptStatusTitle') : isEmptyMerchant ? '' : transactionMerchant; - const amountTitle = isScanning ? translate('iou.receiptStatusTitle') : formattedTransactionAmount ? formattedTransactionAmount.toString() : ''; + const getMerchant = isEmptyMerchant ? '' : transactionMerchant; + const getAmount = formattedTransactionAmount ? formattedTransactionAmount.toString() : ''; + const merchantTitle = isScanning ? translate('iou.receiptStatusTitle') : getMerchant; + const amountTitle = isScanning ? translate('iou.receiptStatusTitle') : getAmount; const saveBillable = useCallback( (newBillable: boolean) => { // If the value hasn't changed, don't request to save changes on the server and just close the modal From 826e766a055000791112ce61b1e48e0222fe6984 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Thu, 25 Apr 2024 11:44:58 -0400 Subject: [PATCH 429/580] chore: disable the toggle button for requiring categorization of expenses --- .../workspace/categories/WorkspaceCategoriesSettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 02891ab87c90..13f7538e9551 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -50,7 +50,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet isActive={policy?.requiresCategory ?? false} onToggle={updateWorkspaceRequiresCategory} pendingAction={policy?.pendingFields?.requiresCategory} - disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions} + disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions || isConnectedToAccounting} wrapperStyle={[styles.mt2, styles.mh4]} errors={policy?.errorFields?.requiresCategory ?? undefined} /> From 50cd3687ab2b378a57db1473e5a21dc45f915b44 Mon Sep 17 00:00:00 2001 From: Robert Kozik <robert.kozik@swmansion.com> Date: Thu, 25 Apr 2024 21:08:30 +0200 Subject: [PATCH 430/580] change line height based on composer emoji value --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 8f42da5a1575..95e3c038dfc0 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -52,6 +52,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import variables from '@styles/variables'; type SyncSelection = { position: number; @@ -730,6 +731,11 @@ function ComposerWithSuggestions( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const isOnlyEmojiLineHeight = useMemo(() => { + const isOnlyEmoji = EmojiUtils.containsOnlyEmojis(value); + return isOnlyEmoji ? {lineHeight: variables.fontSizeOnlyEmojisHeight} : {}; + }, [value]); + return ( <> <View style={[StyleUtils.getContainerComposeStyles(), styles.textInputComposeBorder]}> @@ -743,7 +749,7 @@ function ComposerWithSuggestions( onChangeText={onChangeText} onKeyPress={triggerHotkeyActions} textAlignVertical="top" - style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} + style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose, isOnlyEmojiLineHeight]} maxLines={maxComposerLines} onFocus={onFocus} onBlur={onBlur} From 35b23f6b6247aaeef6e90a488347e6cf02811ff6 Mon Sep 17 00:00:00 2001 From: Aldo Canepa <aldo@expensify.com> Date: Thu, 25 Apr 2024 12:16:13 -0700 Subject: [PATCH 431/580] Patch for WebView crash --- ...fy+react-native-live-markdown+0.1.62.patch | 9 ++ patches/@rnmapbox+maps+10.1.11.patch | 114 ++++++++++++++++++ ...native+0.73.4+015+fixIOSWebViewCrash.patch | 24 ++++ 3 files changed, 147 insertions(+) create mode 100644 patches/@expensify+react-native-live-markdown+0.1.62.patch create mode 100644 patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch diff --git a/patches/@expensify+react-native-live-markdown+0.1.62.patch b/patches/@expensify+react-native-live-markdown+0.1.62.patch new file mode 100644 index 000000000000..0d2c962efd0f --- /dev/null +++ b/patches/@expensify+react-native-live-markdown+0.1.62.patch @@ -0,0 +1,9 @@ +diff --git a/node_modules/@expensify/react-native-live-markdown/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m b/node_modules/@expensify/react-native-live-markdown/ios/RCTBackedTextFieldDelegateAdapter+Markdown.mm +similarity index 100% +rename from node_modules/@expensify/react-native-live-markdown/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m +rename to node_modules/@expensify/react-native-live-markdown/ios/RCTBackedTextFieldDelegateAdapter+Markdown.mm +diff --git a/node_modules/@expensify/react-native-live-markdown/ios/RCTBaseTextInputView+Markdown.m b/node_modules/@expensify/react-native-live-markdown/ios/RCTBaseTextInputView+Markdown.mm +similarity index 100% +rename from node_modules/@expensify/react-native-live-markdown/ios/RCTBaseTextInputView+Markdown.m +rename to node_modules/@expensify/react-native-live-markdown/ios/RCTBaseTextInputView+Markdown.mm + diff --git a/patches/@rnmapbox+maps+10.1.11.patch b/patches/@rnmapbox+maps+10.1.11.patch index 9f2df5f4ee6e..5c5b8f0b69bb 100644 --- a/patches/@rnmapbox+maps+10.1.11.patch +++ b/patches/@rnmapbox+maps+10.1.11.patch @@ -11,3 +11,117 @@ index dbd6d0b..1d043f2 100644 val map = mapView.getMapboxMap() it.setDuration(0) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m +index 1808393..ec00542 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTEventEmitter.h> + + @interface RCT_EXTERN_MODULE(RNMBXOfflineModule, RCTEventEmitter<RCTBridgeModule>) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m +index 550f67b..76da02d 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTEventEmitter.h> + + @interface RCT_EXTERN_MODULE(RNMBXOfflineModuleLegacy, RCTEventEmitter<RCTBridgeModule>) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m +index a98e102..e43be8f 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/Offline/RNMBXTileStoreModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTEventEmitter.h> + + @interface RCT_EXTERN_MODULE(RNMBXTileStoreModule, NSObject) +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m +index 62205d5..1db2ac4 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCalloutViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTViewManager.h> + #import <Foundation/Foundation.h> + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m +index e23b10c..6a023fa 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTViewManager.h> + #import <Foundation/Foundation.h> + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m +index 8b89774..9f85c35 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLocationModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTEventEmitter.h> + + @class RNMBXLocation; +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m +index d7c05de..f680b86 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXLogging.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + + @interface RCT_EXTERN_MODULE(RNMBXLogging, NSObject) + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m +index 72f9928..f4f5fe2 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewContentManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTViewManager.h> + #import <Foundation/Foundation.h> + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m +index c0ab14d..6177811 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXMarkerViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTViewManager.h> + #import <Foundation/Foundation.h> + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m +index 3b0af79..e00b508 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXModule.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + + @interface RCT_EXTERN_MODULE(RNMBXModule, NSObject) + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m +index 6fa19e5..54d0ff9 100644 +--- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m ++++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXPointAnnotationViewManager.m +@@ -1,4 +1,4 @@ +-#import "React/RCTBridgeModule.h" ++#import <React/RCTBridgeModule.h> + #import <React/RCTViewManager.h> + #import <Foundation/Foundation.h> + +diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.m b/node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.mm +similarity index 100% +rename from node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.m +rename to node_modules/@rnmapbox/maps/ios/RNMBX/ShapeAnimators/RNMBXMovePointShapeAnimatorModule.mm diff --git a/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch b/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch new file mode 100644 index 000000000000..7c4244f3a811 --- /dev/null +++ b/patches/react-native+0.73.4+015+fixIOSWebViewCrash.patch @@ -0,0 +1,24 @@ +diff --git a/node_modules/react-native/scripts/cocoapods/new_architecture.rb b/node_modules/react-native/scripts/cocoapods/new_architecture.rb +index ba75b019a9b9b2..c9999beb82b7ea 100644 +--- a/node_modules/react-native/scripts/cocoapods/new_architecture.rb ++++ b/node_modules/react-native/scripts/cocoapods/new_architecture.rb +@@ -105,6 +105,10 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version) + current_headers = current_config["HEADER_SEARCH_PATHS"] != nil ? current_config["HEADER_SEARCH_PATHS"] : "" + current_cpp_flags = current_config["OTHER_CPLUSPLUSFLAGS"] != nil ? current_config["OTHER_CPLUSPLUSFLAGS"] : "" + ++ flags_to_add = new_arch_enabled ? ++ "#{@@folly_compiler_flags} -DRCT_NEW_ARCH_ENABLED=1" : ++ "#{@@folly_compiler_flags}" ++ + header_search_paths = ["\"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/Headers/Private/Yoga\""] + if ENV['USE_FRAMEWORKS'] + header_search_paths << "\"$(PODS_ROOT)/DoubleConversion\"" +@@ -124,7 +128,7 @@ def self.install_modules_dependencies(spec, new_arch_enabled, folly_version) + } + end + header_search_paths_string = header_search_paths.join(" ") +- spec.compiler_flags = compiler_flags.empty? ? @@folly_compiler_flags : "#{compiler_flags} #{@@folly_compiler_flags}" ++ spec.compiler_flags = compiler_flags.empty? ? "$(inherited) #{flags_to_add}" : "$(inherited) #{compiler_flags} #{flags_to_add}" + current_config["HEADER_SEARCH_PATHS"] = current_headers.empty? ? + header_search_paths_string : + "#{current_headers} #{header_search_paths_string}" From 148c2b959f7add4baca20b54967eb533022e6386 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Fri, 26 Apr 2024 02:14:59 +0530 Subject: [PATCH 432/580] fix: no space between audit message and notes violations. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index f0b0f3bef613..e0d324db845e 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -384,6 +384,7 @@ function MoneyRequestView({ } /> )} + {!(!hasReceipt && (canEditReceipt || isAdmin || isApprover)) && !(showMapAsImage || hasReceipt) && <View style={{marginVertical: 6}} />} {shouldShowNotesViolations && <ReceiptAuditMessages notes={noteTypeViolations} />} <ViolationMessages violations={getViolationsForField('receipt')} /> <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> From f022c9323cc466ae833b4ebc9f8e455f289470f6 Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Thu, 25 Apr 2024 19:30:44 -0400 Subject: [PATCH 433/580] fix: allow the user to clear the error --- .../workspace/categories/WorkspaceCategoriesSettingsPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 13f7538e9551..fe07c26e686d 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -13,6 +13,7 @@ import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccess import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -53,6 +54,7 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet disabled={!policy?.areCategoriesEnabled || !hasEnabledOptions || isConnectedToAccounting} wrapperStyle={[styles.mt2, styles.mh4]} errors={policy?.errorFields?.requiresCategory ?? undefined} + onCloseError={() => Policy.clearPolicyErrorField(policy?.id ?? '', 'requiresCategory')} /> </View> </ScreenWrapper> From b0e2defc69269cf7a43cddd6ef6717841d744181 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa <rodrigo@expensify.com> Date: Thu, 25 Apr 2024 20:31:19 -0300 Subject: [PATCH 434/580] replaceing [] with {} for object.keys --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7542ca12c592..1e9c4a2a3c1b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -738,7 +738,7 @@ function openReport( if (ReportUtils.isGroupChat(newReportObject)) { parameters.chatType = CONST.REPORT.CHAT_TYPE.GROUP; parameters.groupChatAdminLogins = currentUserEmail; - parameters.optimisticAccountIDList = Object.keys(newReportObject.participants ?? []).join(','); + parameters.optimisticAccountIDList = Object.keys(newReportObject.participants ?? {}).join(','); parameters.reportName = newReportObject.reportName ?? ''; // If we have an avatar then include it with the parameters From 400109cb7f5166af2c984fbe870272191bc58844 Mon Sep 17 00:00:00 2001 From: Francois Laithier <francois@expensify.com> Date: Thu, 25 Apr 2024 17:55:54 -0700 Subject: [PATCH 435/580] Disallow blank first names, allow blank surnames during onboarding --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - .../BaseOnboardingPersonalDetails.tsx | 5 +---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 84f368dd6df7..e45b6f73d3d2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1338,7 +1338,6 @@ export default { }, error: { requiredFirstName: 'Please input your first name to continue', - requiredLastName: 'Please input your last name to continue', }, }, featureTraining: { diff --git a/src/languages/es.ts b/src/languages/es.ts index f3aedb75690d..46b74ee15983 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1337,7 +1337,6 @@ export default { }, error: { requiredFirstName: 'Introduce tu nombre para continuar', - requiredLastName: 'Introduce tu apellido para continuar', }, }, featureTraining: { diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index 7049b04cc293..c8e354a93768 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -90,7 +90,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat const errors = {}; // First we validate the first name field - if (values.firstName.length === 0) { + if (values.firstName.replace(CONST.REGEX.ANY_SPACE, '').length === 0) { ErrorUtils.addErrorMessage(errors, 'firstName', 'onboarding.error.requiredFirstName'); } if (!ValidationUtils.isValidDisplayName(values.firstName)) { @@ -103,9 +103,6 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat } // Then we validate the last name field - if (values.lastName.length === 0) { - ErrorUtils.addErrorMessage(errors, 'lastName', 'onboarding.error.requiredLastName'); - } if (!ValidationUtils.isValidDisplayName(values.lastName)) { ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.hasInvalidCharacter'); } else if (values.lastName.length > CONST.DISPLAY_NAME.MAX_LENGTH) { From 9b2d16d86a7c7f639911a72f4c21d07b855e5b2c Mon Sep 17 00:00:00 2001 From: Janic Duplessis <janicduplessis@gmail.com> Date: Thu, 25 Apr 2024 22:35:33 -0400 Subject: [PATCH 436/580] Don't call GetNewerActions when not comment linking --- src/pages/home/report/ReportActionsView.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 26f796b8bdc4..e58e0fa3312e 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -348,10 +348,17 @@ function ReportActionsView({ newestReportAction: newestReportAction.pendingAction, firstReportActionID: newestReportAction?.reportActionID, isLoadingOlderReportsFirstNeeded, + reportActionID, })}`, ); - if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + if ( + !reportActionID || + isLoadingInitialReportActions || + isLoadingOlderReportActions || + network.isOffline || + newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ) { return; } From 3fc5ecf392de139374923b2b303a56c8f536884f Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 26 Apr 2024 10:05:37 +0700 Subject: [PATCH 437/580] fix lint --- src/components/Button/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index ce012d650512..b6e20164348b 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -209,7 +209,7 @@ function Button( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - + const renderContent = () => { if ('children' in rest) { return rest.children; From 05fd71550782d9b27637a3c5035f0279b32f9224 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Fri, 26 Apr 2024 11:14:57 +0800 Subject: [PATCH 438/580] subs to the quick action report and policy onyx --- .../FloatingActionButtonAndPopover.tsx | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index adc5f3517c11..428b0d52b6f5 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -54,6 +54,12 @@ type FloatingActionButtonAndPopoverOnyxProps = { /** Information on the last taken action to display as Quick Action */ quickAction: OnyxEntry<OnyxTypes.QuickAction>; + /** The report data of the quick action */ + quickActionReport: OnyxEntry<OnyxTypes.Report>; + + /** The policy data of the quick action */ + quickActionPolicy: OnyxEntry<OnyxTypes.Policy>; + /** The current session */ session: OnyxEntry<OnyxTypes.Session>; @@ -141,7 +147,18 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => { * FAB that can open or close the menu. */ function FloatingActionButtonAndPopover( - {onHideCreateMenu, onShowCreateMenu, isLoading = false, allPolicies, quickAction, session, personalDetails, hasSeenTrackTraining}: FloatingActionButtonAndPopoverProps, + { + onHideCreateMenu, + onShowCreateMenu, + isLoading = false, + allPolicies, + quickAction, + quickActionReport, + quickActionPolicy, + session, + personalDetails, + hasSeenTrackTraining, + }: FloatingActionButtonAndPopoverProps, ref: ForwardedRef<FloatingActionButtonAndPopoverRef>, ) { const styles = useThemeStyles(); @@ -154,10 +171,6 @@ function FloatingActionButtonAndPopover( const prevIsFocused = usePrevious(isFocused); const {isOffline} = useNetwork(); - const quickActionReport: OnyxEntry<OnyxTypes.Report> = useMemo(() => (quickAction ? ReportUtils.getReport(quickAction.chatReportID) : null), [quickAction]); - - const quickActionPolicy = allPolicies ? allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`] : undefined; - const quickActionAvatars = useMemo(() => { if (quickActionReport) { const avatars = ReportUtils.getIcons(quickActionReport, personalDetails); @@ -418,6 +431,12 @@ export default withOnyx<FloatingActionButtonAndPopoverProps & RefAttributes<Floa quickAction: { key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, }, + quickActionReport: { + key: ({quickAction}) => `${ONYXKEYS.COLLECTION.REPORT}${quickAction?.chatReportID}`, + }, + quickActionPolicy: { + key: ({quickActionReport}) => `${ONYXKEYS.COLLECTION.POLICY}${quickActionReport?.policyID}`, + }, personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, From 3dffd6404e2166aae51fcffd10853936583509a6 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus <bernhard.josephus@gmail.com> Date: Fri, 26 Apr 2024 13:16:58 +0800 Subject: [PATCH 439/580] remove console log --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index edd2155a3c55..9927abb134a0 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -389,7 +389,6 @@ function ReimbursementAccountPage({ ].some((value) => value === currentStep) ); - console.log('is loading?', {isLoading, isReimbursementAccountLoading, hasACHDataBeenLoaded, plaidCurrentEvent, substep: achData?.subStep}) // Show loading indicator when page is first time being opened and props.reimbursementAccount yet to be loaded from the server // or when data is being loaded. Don't show the loading indicator if we're offline and restarted the bank account setup process // On Android, when we open the app from the background, Onfido activity gets destroyed, so we need to reopen it. From ff30cb544dbfd8b9c044a2b3e09ea43c0d7f117f Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Fri, 26 Apr 2024 07:44:52 +0200 Subject: [PATCH 440/580] Add test data to SearchPage --- src/pages/Search/SearchPage.tsx | 49 ++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index a48192bdd68d..2ddea460de10 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -19,6 +19,53 @@ import useCustomBackHandler from './useCustomBackHandler'; type SearchPageProps = StackScreenProps<CentralPaneNavigatorParamList, typeof SCREENS.SEARCH.CENTRAL_PANE>; +const data = [ + { + receipt: {source: 'http...'}, + hasEReceipt: false, + created: '2024-04-11 00:00:00', + amount: 12500, + type: 'cash', + reportID: '1', + transactionThreadReportID: '2', + transactionID: '1234', + modifiedCreated: '2024-05-06 00:00:00', + description: 'description description description description', + from: { + displayName: 'TestUser1', + avatarUrl: '', + }, + to: { + displayName: 'TestUser2', + avatarUrl: '', + }, + category: 'Bananas', + tag: 'Green', + }, + { + receipt: {source: 'http...'}, + hasEReceipt: false, + created: '2024-04-11 00:00:00', + amount: 12500, + type: 'cash', // not present in live data (data outside of snapshot_) + reportID: '1', + transactionThreadReportID: '2', + transactionID: '5555', + modifiedCreated: '2024-05-06 00:00:00', + description: 'description', + from: { + displayName: 'TestUser1', + avatarUrl: '', + }, + to: { + displayName: 'TestUser2', + avatarUrl: '', + }, + category: 'Bananas', + tag: 'Green', + }, +]; + function SearchPage({route}: SearchPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -57,7 +104,7 @@ function SearchPage({route}: SearchPageProps) { ListItem={ExpenseListItem} onSelectRow={() => {}} onSelectAll={() => {}} - sections={[{data: [], isDisabled: false}]} + sections={[{data, isDisabled: false}]} onCheckboxPress={() => {}} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} From 259e557ddf82b5d861482fa4099a977213812044 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 26 Apr 2024 13:22:56 +0700 Subject: [PATCH 441/580] send splitPayerAccountIDs in split and open report API --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 20aaf5655ae7..ae48a274665a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3613,7 +3613,7 @@ function splitBillAndOpenReport({ createdReportActionID: splitData.createdReportActionID, policyID: splitData.policyID, chatType: splitData.chatType, - splitPayerAccountIDs: [], + splitPayerAccountIDs, }; API.write(WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT, parameters, onyxData); From 117e5502753c28a199249941516d263ac9a19e09 Mon Sep 17 00:00:00 2001 From: Aldo Canepa <aldo@expensify.com> Date: Thu, 25 Apr 2024 23:23:16 -0700 Subject: [PATCH 442/580] Add translations for sync job --- src/CONST.ts | 6 ++++++ src/languages/en.ts | 12 ++++++++++++ src/languages/es.ts | 12 ++++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 2e14aa7cf21f..1aeff17bd0d9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1681,6 +1681,12 @@ const CONST = { QBO_SYNC_PAYMENTS: 'quickbooksOnlineSyncBillPayments', QBO_IMPORT_TAX_CODES: 'quickbooksOnlineSyncTaxCodes', QBO_CHECK_CONNECTION: 'quickbooksOnlineCheckConnection', + QBO_SYNC_TITLE: 'quickbooksOnlineSyncTitle', + QBO_SYNC_LOAD_DATA: 'quickbooksOnlineSyncLoadData', + QBO_SYNC_APPLY_CATEGORIES: 'quickbooksOnlineSyncApplyCategories', + QBO_SYNC_APPLY_CUSTOMERS: 'quickbooksOnlineSyncApplyCustomers', + QBO_SYNC_APPLY_PEOPLE: 'quickbooksOnlineSyncApplyEmployees', + QBO_SYNC_APPLY_CLASSES_LOCATIONS: 'quickbooksOnlineSyncApplyClassesLocations', JOB_DONE: 'jobDone', }, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index 84f368dd6df7..feeb09c6fa77 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2196,6 +2196,18 @@ export default { return 'Importing your QuickBooks Online data'; case 'startingImport': return 'Importing your QuickBooks Online data'; + case 'quickbooksOnlineSyncTitle': + return 'Synchronizing QuickBooks Online data'; + case 'quickbooksOnlineSyncLoadData': + return 'Loading data'; + case 'quickbooksOnlineSyncApplyCategories': + return 'Updating categories'; + case 'quickbooksOnlineSyncApplyCustomers': + return 'Updating Customers/Projects'; + case 'quickbooksOnlineSyncApplyEmployees': + return 'Updating people list'; + case 'quickbooksOnlineSyncApplyClassesLocations': + return 'Updating report fields'; default: { return `Translation missing for stage: ${stage}`; } diff --git a/src/languages/es.ts b/src/languages/es.ts index f3aedb75690d..224965f130c0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2191,6 +2191,18 @@ export default { return 'Importando datos desde QuickBooks Online'; case 'startingImport': return 'Importando datos desde QuickBooks Online'; + case 'quickbooksOnlineSyncTitle': + return 'Sincronizando datos desde QuickBooks Online'; + case 'quickbooksOnlineSyncLoadData': + return 'Cargando datos'; + case 'quickbooksOnlineSyncApplyCategories': + return 'Actualizando categorías'; + case 'quickbooksOnlineSyncApplyCustomers': + return 'Actualizando Clientes/Proyectos'; + case 'quickbooksOnlineSyncApplyEmployees': + return 'Actualizando empleados'; + case 'quickbooksOnlineSyncApplyClassesLocations': + return 'Actualizando clases'; default: { return `Translation missing for stage: ${stage}`; } From 4d04342193429bd2a856a95cf92406f93e2d752c Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 26 Apr 2024 14:20:03 +0700 Subject: [PATCH 443/580] fix: removew should show --- .../AdminPolicyAccessOrNotFoundWrapper.tsx | 7 ++--- .../workspace/WorkspaceInviteMessagePage.tsx | 1 - src/pages/workspace/WorkspaceInvitePage.tsx | 3 +- .../workspace/WorkspacePageWithSections.tsx | 29 ++++++++++--------- .../WorkspaceProfileCurrencyPage.tsx | 3 +- 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx index e658522ebd5a..207c277a65d1 100644 --- a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx @@ -35,9 +35,6 @@ type AdminPolicyAccessOrNotFoundComponentProps = AdminAccessOrNotFoundOnyxProps /** The key in the translations file to use for the subtitle */ subtitleKey?: TranslationPaths; - - /** If true, child components are replaced with a blocking "not found" view */ - shouldShow?: boolean; }; function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFoundComponentProps) { @@ -55,7 +52,7 @@ function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFound const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyAdmin(props.policy); + const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPendingDeletePolicy(props.policy); if (shouldShowFullScreenLoadingIndicator) { return <FullscreenLoadingIndicator />; @@ -68,7 +65,7 @@ function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFound return ( <ScreenWrapper testID={AdminPolicyAccessOrNotFoundComponent.displayName}> <FullPageNotFoundView - shouldShow={props.shouldShow ?? true} + shouldShow shouldForceFullScreen onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} onLinkPress={props.onLinkPress} diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 213b06769dff..84ee1e1bf70f 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -135,7 +135,6 @@ function WorkspaceInviteMessagePage({ return ( <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID} - shouldShow={isEmptyObject(policy) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} onLinkPress={PolicyUtils.goBackFromInvalidPolicy} > diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 53194008688d..08799d4bf5ac 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -52,7 +52,7 @@ type WorkspaceInvitePageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceInvitePageOnyxProps & StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.INVITE>; -function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, policy, isLoadingReportData = true}: WorkspaceInvitePageProps) { +function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, policy}: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [searchTerm, setSearchTerm] = useState(''); @@ -282,7 +282,6 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli return ( <AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID} - shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} onLinkPress={PolicyUtils.goBackFromInvalidPolicy} > diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index adc986b2ff78..4889c1dbe350 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -4,6 +4,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -22,7 +23,6 @@ import type {Route} from '@src/ROUTES'; import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -156,18 +156,19 @@ function WorkspacePageWithSections({ }, [policy, shouldShowNonAdmin]); return ( - <AdminPolicyAccessOrNotFoundWrapper - policyID={route.params.policyID ?? ''} - onLinkPress={Navigation.resetToHome} - shouldShow={shouldShow} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + shouldEnablePickerAvoiding={false} + shouldEnableMaxHeight + testID={WorkspacePageWithSections.displayName} + shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - shouldEnablePickerAvoiding={false} - shouldEnableMaxHeight - testID={WorkspacePageWithSections.displayName} - shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow} + <FullPageNotFoundView + onBackButtonPress={Navigation.dismissModal} + onLinkPress={Navigation.resetToHome} + shouldShow={shouldShow} + subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + shouldForceFullScreen > <HeaderWithBackButton title={headerText} @@ -194,8 +195,8 @@ function WorkspacePageWithSections({ {footer} </> )} - </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </FullPageNotFoundView> + </ScreenWrapper> ); } diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index d2b5bdd080ee..20ffe410a196 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -31,7 +31,7 @@ type WorkspaceProfileCurrencyPageSectionItem = { const getDisplayText = (currencyCode: string, currencySymbol: string) => `${currencyCode} - ${currencySymbol}`; -function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingReportData = true}: WorkspaceProfileCurrentPageProps) { +function WorkspaceProfileCurrencyPage({currencyList = {}, policy}: WorkspaceProfileCurrentPageProps) { const {translate} = useLocalize(); const [searchText, setSearchText] = useState(''); const trimmedText = searchText.trim().toLowerCase(); @@ -74,7 +74,6 @@ function WorkspaceProfileCurrencyPage({currencyList = {}, policy, isLoadingRepor <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''} onLinkPress={PolicyUtils.goBackFromInvalidPolicy} - shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || !PolicyUtils.isPolicyAdmin(policy) || PolicyUtils.isPendingDeletePolicy(policy)} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > <ScreenWrapper From e22c3eb565a51ad21ddc94eaaba237cce3358a2d Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 26 Apr 2024 09:54:33 +0200 Subject: [PATCH 444/580] Update invoice-generic.svg icon --- assets/images/invoice-generic.svg | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/assets/images/invoice-generic.svg b/assets/images/invoice-generic.svg index 171b892fd8b3..d0e2662c4084 100644 --- a/assets/images/invoice-generic.svg +++ b/assets/images/invoice-generic.svg @@ -1,4 +1,15 @@ -<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M6.75 7C6.33579 7 6 7.33579 6 7.75C6 8.16421 6.33579 8.5 6.75 8.5H13.25C13.6642 8.5 14 8.16421 14 7.75C14 7.33579 13.6642 7 13.25 7H6.75Z" fill="#03D47C"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3.8254 1.70739L4.75 2.49991L6.1746 1.27882C6.36185 1.11832 6.63815 1.11832 6.8254 1.27882L8.25 2.49991L9.6746 1.27882C9.86185 1.11832 10.1382 1.11832 10.3254 1.27882L11.75 2.49991L13.1746 1.27882C13.3618 1.11832 13.6382 1.11832 13.8254 1.27882L15.25 2.49991L16.1746 1.70739C16.4989 1.42939 17 1.65984 17 2.08702V7.1875L17.6306 7.69986C17.8643 7.88975 18 8.17484 18 8.47597V18C18 19.1046 17.1046 20 16 20H4C2.89543 20 2 19.1046 2 18V8.47597C2 8.17484 2.1357 7.88975 2.36941 7.69986L3 7.1875V2.08702C3 1.65984 3.50106 1.42939 3.8254 1.70739ZM5 4.91975V9.52781L10 12L15 9.52781V4.91975L13.5 3.63403L11.75 5.13403L10 3.63403L8.25 5.13403L6.5 3.63403L5 4.91975Z" fill="#03D47C"/> +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 28.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve"> +<style type="text/css"> + .st0{fill-rule:evenodd;clip-rule:evenodd;} +</style> +<path d="M6.75,7C6.336,7,6,7.336,6,7.75C6,8.164,6.336,8.5,6.75,8.5h6.5C13.664,8.5,14,8.164,14,7.75C14,7.336,13.664,7,13.25,7 + H6.75z"/> +<path class="st0" d="M3.825,1.707L4.75,2.5l1.425-1.221c0.187-0.161,0.464-0.161,0.651,0L8.25,2.5l1.425-1.221 + c0.187-0.161,0.464-0.161,0.651,0L11.75,2.5l1.425-1.221c0.187-0.161,0.464-0.161,0.651,0L15.25,2.5l0.925-0.793 + C16.499,1.429,17,1.66,17,2.087v5.1L17.631,7.7C17.864,7.89,18,8.175,18,8.476V18c0,1.105-0.895,2-2,2H4c-1.105,0-2-0.895-2-2V8.476 + C2,8.175,2.136,7.89,2.369,7.7L3,7.188v-5.1C3,1.66,3.501,1.429,3.825,1.707z M5,4.92v4.608L10,12l5-2.472V4.92l-1.5-1.286 + l-1.75,1.5L10,3.634l-1.75,1.5l-1.75-1.5L5,4.92z"/> </svg> From 84a54db10fdc3b11c287186a61d43e90d7c1f1a5 Mon Sep 17 00:00:00 2001 From: war-in <war-in@users.noreply.github.com> Date: Fri, 26 Apr 2024 10:03:37 +0200 Subject: [PATCH 445/580] review suggestions --- .../ReportActionItem/MoneyRequestView.tsx | 2 +- src/hooks/useHandleExceedMaxCommentLength.ts | 5 ++-- src/libs/ReportUtils.ts | 24 ++++++++++++------- .../ReportActionCompose.tsx | 2 +- .../report/ReportActionItemMessageEdit.tsx | 4 ++-- .../request/step/IOURequestStepCategory.tsx | 2 +- .../request/step/IOURequestStepMerchant.tsx | 2 +- .../iou/request/step/IOURequestStepTag.tsx | 2 +- 8 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 08bcc16cbbee..bfa34efb4f05 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -156,7 +156,7 @@ function MoneyRequestView({ // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat - const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); + const isPolicyExpenseChat = ReportUtils.isReportInGroupPolicy(report); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTagList), [policyTagList]); diff --git a/src/hooks/useHandleExceedMaxCommentLength.ts b/src/hooks/useHandleExceedMaxCommentLength.ts index fea0793c9854..69c1d7597164 100644 --- a/src/hooks/useHandleExceedMaxCommentLength.ts +++ b/src/hooks/useHandleExceedMaxCommentLength.ts @@ -1,14 +1,15 @@ import _ from 'lodash'; import {useCallback, useMemo, useState} from 'react'; import * as ReportUtils from '@libs/ReportUtils'; +import type {ParsingDetails} from '@libs/ReportUtils'; import CONST from '@src/CONST'; const useHandleExceedMaxCommentLength = () => { const [hasExceededMaxCommentLength, setHasExceededMaxCommentLength] = useState(false); const handleValueChange = useCallback( - (value: string) => { - if (ReportUtils.getCommentLength(value) <= CONST.MAX_COMMENT_LENGTH) { + (value: string, parsingDetails?: ParsingDetails) => { + if (ReportUtils.getCommentLength(value, parsingDetails) <= CONST.MAX_COMMENT_LENGTH) { if (hasExceededMaxCommentLength) { setHasExceededMaxCommentLength(false); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8ee14d07f856..4713444d0088 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -874,12 +874,19 @@ function isControlPolicyExpenseChat(report: OnyxEntry<Report>): boolean { return isPolicyExpenseChat(report) && getPolicyType(report, allPolicies) === CONST.POLICY.TYPE.CORPORATE; } +/** + * Whether the provided policyType is a Free, Collect or Control policy type + */ +function isGroupPolicy(policyType: string): boolean { + return policyType === CONST.POLICY.TYPE.CORPORATE || policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.FREE; +} + /** * Whether the provided report belongs to a Free, Collect or Control policy */ -function isGroupPolicy(report: OnyxEntry<Report>): boolean { +function isReportInGroupPolicy(report: OnyxEntry<Report>): boolean { const policyType = getPolicyType(report, allPolicies); - return policyType === CONST.POLICY.TYPE.CORPORATE || policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.FREE; + return isGroupPolicy(policyType); } /** @@ -1421,7 +1428,7 @@ function canAddOrDeleteTransactions(moneyRequestReport: OnyxEntry<Report>): bool return false; } - if (isGroupPolicy(moneyRequestReport) && isProcessingReport(moneyRequestReport) && !PolicyUtils.isInstantSubmitEnabled(getPolicy(moneyRequestReport?.policyID))) { + if (isReportInGroupPolicy(moneyRequestReport) && isProcessingReport(moneyRequestReport) && !PolicyUtils.isInstantSubmitEnabled(getPolicy(moneyRequestReport?.policyID))) { return false; } @@ -3186,11 +3193,11 @@ function getParsedComment(text: string, parsingDetails?: ParsingDetails): string let isGroupPolicyReport = false; if (parsingDetails?.reportID) { const currentReport = getReport(parsingDetails?.reportID); - isGroupPolicyReport = currentReport && !isEmptyObject(currentReport) ? isGroupPolicy(currentReport) : false; + isGroupPolicyReport = isReportInGroupPolicy(currentReport); } if (parsingDetails?.policyID) { const policyType = getPolicy(parsingDetails?.policyID).type; - isGroupPolicyReport = policyType === CONST.POLICY.TYPE.CORPORATE || policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.FREE; + isGroupPolicyReport = isGroupPolicy(policyType); } const parser = new ExpensiMark(); @@ -5161,7 +5168,7 @@ function canRequestMoney(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>, o // which is tied to their workspace chat. if (isMoneyRequestReport(report)) { const canAddTransactions = canAddOrDeleteTransactions(report); - return isGroupPolicy(report) ? isOwnPolicyExpenseChat && canAddTransactions : canAddTransactions; + return isReportInGroupPolicy(report) ? isOwnPolicyExpenseChat && canAddTransactions : canAddTransactions; } // In the case of policy expense chat, users can only submit expenses from their own policy expense chat @@ -6027,7 +6034,7 @@ function canBeAutoReimbursed(report: OnyxEntry<Report>, policy: OnyxEntry<Policy const reimbursableTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const autoReimbursementLimit = policy.autoReimbursementLimit ?? 0; const isAutoReimbursable = - isGroupPolicy(report) && + isReportInGroupPolicy(report) && policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES && autoReimbursementLimit >= reimbursableTotal && reimbursableTotal > 0 && @@ -6385,7 +6392,7 @@ export { isExpensifyOnlyParticipantInReport, isGroupChat, isGroupChatAdmin, - isGroupPolicy, + isReportInGroupPolicy, isHoldCreator, isIOUOwnedByCurrentUser, isIOUReport, @@ -6463,4 +6470,5 @@ export type { OptimisticTaskReportAction, OptionData, TransactionDetails, + ParsingDetails, }; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 75d0c703b5b1..5bfa2475ee23 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -461,7 +461,7 @@ function ReportActionCompose({ if (value.length === 0 && isComposerFullSize) { Report.setIsComposerFullSize(reportID, false); } - validateCommentMaxLength(value); + validateCommentMaxLength(value, {reportID}); }} /> <ReportDropUI diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index bfd2c8b5ca7f..97cad278dff2 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -380,8 +380,8 @@ function ReportActionItemMessageEdit( const focus = focusComposerWithDelay(textInputRef.current); useEffect(() => { - validateCommentMaxLength(draft); - }, [draft, validateCommentMaxLength]); + validateCommentMaxLength(draft, {reportID}); + }, [draft, reportID, validateCommentMaxLength]); return ( <> diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index a571192f7a47..4b34a6a19600 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -73,7 +73,7 @@ function IOURequestStepCategory({ // The transactionCategory can be an empty string, so to maintain the logic we'd like to keep it in this shape until utils refactor // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowCategory = ReportUtils.isGroupPolicy(report) && (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); + const shouldShowCategory = ReportUtils.isReportInGroupPolicy(report) && (!!transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {}))); const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.tsx b/src/pages/iou/request/step/IOURequestStepMerchant.tsx index b50495ac47bd..bc6f71b23228 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.tsx +++ b/src/pages/iou/request/step/IOURequestStepMerchant.tsx @@ -63,7 +63,7 @@ function IOURequestStepMerchant({ const merchant = ReportUtils.getTransactionDetails(isEditingSplitBill && !isEmptyObject(splitDraftTransaction) ? splitDraftTransaction : transaction)?.merchant; const isEmptyMerchant = merchant === '' || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const isMerchantRequired = ReportUtils.isGroupPolicy(report) || transaction?.participants?.some((participant) => Boolean(participant.isPolicyExpenseChat)); + const isMerchantRequired = ReportUtils.isReportInGroupPolicy(report) || transaction?.participants?.some((participant) => Boolean(participant.isPolicyExpenseChat)); const navigateBack = () => { Navigation.goBack(backTo); }; diff --git a/src/pages/iou/request/step/IOURequestStepTag.tsx b/src/pages/iou/request/step/IOURequestStepTag.tsx index a62720cbd13a..ff1a1c01600d 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.tsx +++ b/src/pages/iou/request/step/IOURequestStepTag.tsx @@ -77,7 +77,7 @@ function IOURequestStepTag({ const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); - const shouldShowTag = ReportUtils.isGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); + const shouldShowTag = ReportUtils.isReportInGroupPolicy(report) && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists)); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = !shouldShowTag || (isEditing && (isSplitBill ? !canEditSplitBill : reportAction && !canEditMoneyRequest(reportAction))); From c852145ed838a9b4b2caffef35cfeed01f2379a6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 10:22:49 +0200 Subject: [PATCH 446/580] clarify comments --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/ReportUtils.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index a666d81a180e..9491b6141536 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -226,7 +226,7 @@ function isTransactionThread(parentReportAction: OnyxEntry<ReportAction> | Empty * Returns the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions with a childReportID. Returns a reportID if there is exactly one transaction thread for the report, and null otherwise. */ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry<ReportActions> | ReportAction[], isOffline: boolean | undefined = undefined): string | null { - // If the report is not an IOU or Expense report, it shouldn't be treated as one-transaction report. + // If the report is not an IOU, Expense report or an Invoice, it shouldn't be treated as one-transaction report. const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) { return null; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 686808f01611..d61945f506c8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -675,9 +675,6 @@ function isChatReport(report: OnyxEntry<Report> | EmptyObject): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } -/** - * Checks if a report is an invoice report. - */ function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { return report?.type === CONST.REPORT.TYPE.INVOICE; } @@ -869,9 +866,6 @@ function isPolicyExpenseChat(report: OnyxEntry<Report> | Participant | EmptyObje return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || (report?.isPolicyExpenseChat ?? false); } -/** - * Whether the provided report is an invoice room chat. - */ function isInvoiceRoom(report: OnyxEntry<Report>): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; } From f3d02339a9c32f2d893660effa4753ae3dd4d861 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 26 Apr 2024 10:23:18 +0200 Subject: [PATCH 447/580] Use default SectionList checkmark on the SendFrom screen --- .../request/step/IOURequestStepSendFrom.tsx | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index 724909418842..cdc7e9073c5a 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -1,15 +1,11 @@ -import React, {useCallback, useMemo} from 'react'; -import {View} from 'react-native'; +import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; -import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import SelectionList from '@components/SelectionList'; import type {ListItem} from '@components/SelectionList/types'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -39,8 +35,6 @@ type IOURequestStepSendFromProps = IOURequestStepSendFromOnyxProps & function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestStepSendFromProps) { const {translate} = useLocalize(); - const theme = useTheme(); - const styles = useThemeStyles(); const {transactionID, backTo} = route.params; const workspaceOptions: WorkspaceListItem[] = useMemo(() => { @@ -78,20 +72,6 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte navigateBack(); }; - const renderCheckbox = useCallback( - (item: ListItem) => ( - <View style={[styles.roundCheckmarkWrapper, styles.mh2]}> - {item.isSelected && ( - <Icon - src={Expensicons.Checkmark} - fill={theme.success} - /> - )} - </View> - ), - [styles.roundCheckmarkWrapper, styles.mh2, theme.success], - ); - return ( <StepScreenWrapper headerTitle={translate('workspace.invoices.sendFrom')} @@ -104,7 +84,6 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte sections={[{data: workspaceOptions, title: translate('common.workspaces')}]} onSelectRow={selectWorkspace} ListItem={UserListItem} - rightHandSideComponent={renderCheckbox} /> </StepScreenWrapper> ); From 1a24b88541e18566469ced08faec407f7a2e5f2c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 10:26:38 +0200 Subject: [PATCH 448/580] restrict for request options invoice room --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d61945f506c8..150c9c7aebaf 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5270,7 +5270,7 @@ function isGroupChatAdmin(report: OnyxEntry<Report>, accountID: number) { */ function getMoneyRequestOptions(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>, reportParticipants: number[], canUseTrackExpense = true, filterDeprecatedTypes = false): IOUType[] { // In any thread or task report, we do not allow any new expenses yet - if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report))) { + if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report)) || isInvoiceRoom(report)) { return []; } From cc36c78711791d774bd8709cb75728dccf1bf8cf Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 26 Apr 2024 10:30:23 +0200 Subject: [PATCH 449/580] Remove unused style --- src/styles/index.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 90a6ec3bda86..0b994d1780e9 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3074,16 +3074,6 @@ const styles = (theme: ThemeColors) => alignSelf: 'center', }, - roundCheckmarkWrapper: { - alignItems: 'center', - justifyContent: 'center', - height: 28, - width: 28, - borderRadius: 14, - borderColor: theme.borderLighter, - borderWidth: 1, - }, - codeWordWrapper: { ...codeStyles.codeWordWrapper, }, From b024fe556c59a05dba78b8f79566a9199c1f6a56 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 10:34:15 +0200 Subject: [PATCH 450/580] restrict for request options invoice room --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 150c9c7aebaf..a558dd9b941b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5270,7 +5270,7 @@ function isGroupChatAdmin(report: OnyxEntry<Report>, accountID: number) { */ function getMoneyRequestOptions(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>, reportParticipants: number[], canUseTrackExpense = true, filterDeprecatedTypes = false): IOUType[] { // In any thread or task report, we do not allow any new expenses yet - if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report)) || isInvoiceRoom(report)) { + if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report)) || isInvoiceRoom(report) || isInvoiceReport(report)) { return []; } From a85b7570fdfd658129402d288a19fce2bc46e9b0 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 10:52:26 +0200 Subject: [PATCH 451/580] redesign OnboardingReportFooterMessage --- src/components/Banner.tsx | 21 ++++++++++++--- .../report/OnboardingReportFooterMessage.tsx | 26 +++++-------------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx index 94be65f2b9a2..72dc53cceb39 100644 --- a/src/components/Banner.tsx +++ b/src/components/Banner.tsx @@ -7,6 +7,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import getButtonState from '@libs/getButtonState'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; import Hoverable from './Hoverable'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -22,6 +23,9 @@ type BannerProps = { /** Content to display in the banner. */ content?: React.ReactNode; + /** The icon asset to display to the left of the text */ + icon?: IconAsset | null; + /** Should this component render the left-aligned exclamation icon? */ shouldShowIcon?: boolean; @@ -44,7 +48,18 @@ type BannerProps = { textStyles?: StyleProp<TextStyle>; }; -function Banner({text, content, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) { +function Banner({ + text, + content, + icon = Expensicons.Exclamation, + onClose, + onPress, + containerStyles, + textStyles, + shouldRenderHTML = false, + shouldShowIcon = false, + shouldShowCloseButton = false, +}: BannerProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -68,10 +83,10 @@ function Banner({text, content, onClose, onPress, containerStyles, textStyles, s ]} > <View style={[styles.flexRow, styles.flexGrow1, styles.mw100, styles.alignItemsCenter]}> - {shouldShowIcon && ( + {shouldShowIcon && icon && ( <View style={[styles.mr3]}> <Icon - src={Expensicons.Exclamation} + src={icon} fill={StyleUtils.getIconFillColor(getButtonState(shouldHighlight))} /> </View> diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/OnboardingReportFooterMessage.tsx index 21352900968f..41f352c6c032 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/OnboardingReportFooterMessage.tsx @@ -2,6 +2,7 @@ import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Banner from '@components/Banner'; +import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; @@ -45,10 +46,7 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe return ( <> {translate('onboardingBottomMessage.newDotManageTeam.phrase1')} - <TextLink - style={styles.label} - onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminChatReport?.reportID ?? ''))} - > + <TextLink onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminChatReport?.reportID ?? ''))}> {adminChatReport?.reportName ?? CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS} </TextLink> {translate('onboardingBottomMessage.newDotManageTeam.phrase2')} @@ -58,29 +56,19 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe return ( <> {translate('onboardingBottomMessage.default.phrase1')} - <TextLink - style={styles.label} - onPress={() => ReportInstance.navigateToConciergeChat()} - > - {CONST?.CONCIERGE_CHAT_NAME} - </TextLink> + <TextLink onPress={() => ReportInstance.navigateToConciergeChat()}>{CONST?.CONCIERGE_CHAT_NAME}</TextLink> {translate('onboardingBottomMessage.default.phrase2')} </> ); } - }, [adminChatReport?.reportName, adminChatReport?.reportID, choice, styles.label, translate]); + }, [adminChatReport?.reportName, adminChatReport?.reportID, choice, translate]); return ( <Banner containerStyles={[styles.archivedReportFooter]} - content={ - <Text - style={[styles.label, styles.w100, styles.textAlignCenter]} - suppressHighlighting - > - {content} - </Text> - } + shouldShowIcon + icon={Expensicons.Lightbulb} + content={<Text suppressHighlighting>{content}</Text>} /> ); } From 4a92c91913214a3087add6c8db87b24cf1a42b4d Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 10:52:34 +0200 Subject: [PATCH 452/580] update a place --- src/pages/home/report/ReportFooter.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index c36c42a3e36b..e6d30e5c57ee 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -130,10 +130,6 @@ function ReportFooter({ [report.reportID, handleCreateTask], ); - if (isReadOnlyReport) { - return <OnboardingReportFooterMessage />; - } - return ( <> {hideComposer && ( @@ -145,6 +141,7 @@ function ReportFooter({ /> )} {isArchivedRoom && <ArchivedReportFooter report={report} />} + {isReadOnlyReport && <OnboardingReportFooterMessage />} {!isSmallScreenWidth && <View style={styles.offlineIndicatorRow}>{hideComposer && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}</View>} </View> )} From 2f174176464156f4c2f16e7c3be13db99bd0617c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 10:55:05 +0200 Subject: [PATCH 453/580] rename fucntion --- src/components/TaskHeaderActionButton.tsx | 2 +- src/libs/ReportUtils.ts | 10 +++++----- src/libs/actions/Task.ts | 2 +- src/pages/home/HeaderView.tsx | 2 +- src/pages/home/report/ReportActionItem.tsx | 2 +- src/pages/home/report/ReportFooter.tsx | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index a7e3abcd3012..4489232f8f8c 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -25,7 +25,7 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) const {translate} = useLocalize(); const styles = useThemeStyles(); - if (ReportUtils.isReadOnly(report)) { + if (ReportUtils.canWriteInReport(report)) { return null; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2e7fd940bbf2..e82fe722ed89 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1174,9 +1174,9 @@ function isJoinRequestInAdminRoom(report: OnyxEntry<Report>): boolean { } /** - * Checks if report is in read-only mode. + * Checks if the user can write in the provided report */ -function isReadOnly(report: OnyxEntry<Report>): boolean { +function canWriteInReport(report: OnyxEntry<Report>): boolean { if (Array.isArray(report?.permissions)) { return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE); } @@ -1188,7 +1188,7 @@ function isReadOnly(report: OnyxEntry<Report>): boolean { * Checks if the current user is allowed to comment on the given report. */ function isAllowedToComment(report: OnyxEntry<Report>): boolean { - if (isReadOnly(report)) { + if (canWriteInReport(report)) { return false; } @@ -5390,7 +5390,7 @@ function canUserPerformWriteAction(report: OnyxEntry<Report>) { return false; } - return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser && !isReadOnly(report); + return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser && !canWriteInReport(report); } /** @@ -6419,7 +6419,7 @@ export { isValidReport, isValidReportIDFromPath, isWaitingForAssigneeToCompleteTask, - isReadOnly, + canWriteInReport, navigateToDetailsPage, navigateToPrivateNotes, parseReportRouteParams, diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 7a62895f32b4..ab879ef5d689 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1003,7 +1003,7 @@ function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID return true; } - if (ReportUtils.isReadOnly(ReportUtils.getReport(taskReport?.reportID))) { + if (ReportUtils.canWriteInReport(ReportUtils.getReport(taskReport?.reportID))) { return false; } diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 256acb825fb5..c78a8910a98f 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -144,7 +144,7 @@ function HeaderView({ } // Task is not closed - if (!ReportUtils.isReadOnly(report) && report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { + if (!ReportUtils.canWriteInReport(report) && report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { threeDotMenuItems.push({ icon: Expensicons.Trashcan, text: translate('common.delete'), diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index fadd522de15a..6a2ba10f837b 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -915,7 +915,7 @@ function ReportActionItem({ originalReportID={originalReportID ?? ''} isArchivedRoom={ReportUtils.isArchivedRoom(report)} displayAsGroup={displayAsGroup} - disabledActions={ReportUtils.isReadOnly(report) ? RestrictedReadOnlyContextMenuActions : []} + disabledActions={ReportUtils.canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []} isVisible={hovered && draftMessage === undefined && !hasErrors} draftMessage={draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index e6d30e5c57ee..d6141d5152d9 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -82,7 +82,7 @@ function ReportFooter({ const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(report); - const isReadOnlyReport = ReportUtils.isReadOnly(report); + const canWriteInReport = ReportUtils.canWriteInReport(report); const allPersonalDetails = usePersonalDetails(); @@ -141,7 +141,7 @@ function ReportFooter({ /> )} {isArchivedRoom && <ArchivedReportFooter report={report} />} - {isReadOnlyReport && <OnboardingReportFooterMessage />} + {canWriteInReport && <OnboardingReportFooterMessage />} {!isSmallScreenWidth && <View style={styles.offlineIndicatorRow}>{hideComposer && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}</View>} </View> )} From ae9af7fb13c8b3a07f9f134e284ec6cc498ccc87 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 11:08:56 +0200 Subject: [PATCH 454/580] integrate canWriteInReport properly --- src/components/TaskHeaderActionButton.tsx | 2 +- src/libs/ReportUtils.ts | 10 +++++----- src/libs/actions/Task.ts | 2 +- src/pages/home/HeaderView.tsx | 2 +- src/pages/home/report/ReportActionItem.tsx | 2 +- src/pages/home/report/ReportFooter.tsx | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 4489232f8f8c..788734242f7b 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -25,7 +25,7 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) const {translate} = useLocalize(); const styles = useThemeStyles(); - if (ReportUtils.canWriteInReport(report)) { + if (!ReportUtils.canWriteInReport(report)) { return null; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e82fe722ed89..ac5589c81729 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1177,18 +1177,18 @@ function isJoinRequestInAdminRoom(report: OnyxEntry<Report>): boolean { * Checks if the user can write in the provided report */ function canWriteInReport(report: OnyxEntry<Report>): boolean { - if (Array.isArray(report?.permissions)) { - return !report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE); + if (Array.isArray(report?.permissions) && report?.permissions.length > 0) { + return report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE); } - return false; + return true; } /** * Checks if the current user is allowed to comment on the given report. */ function isAllowedToComment(report: OnyxEntry<Report>): boolean { - if (canWriteInReport(report)) { + if (!canWriteInReport(report)) { return false; } @@ -5390,7 +5390,7 @@ function canUserPerformWriteAction(report: OnyxEntry<Report>) { return false; } - return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser && !canWriteInReport(report); + return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser && canWriteInReport(report); } /** diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index ab879ef5d689..251f6e94a3eb 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1003,7 +1003,7 @@ function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID return true; } - if (ReportUtils.canWriteInReport(ReportUtils.getReport(taskReport?.reportID))) { + if (!ReportUtils.canWriteInReport(ReportUtils.getReport(taskReport?.reportID))) { return false; } diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index c78a8910a98f..4295f1f0c46a 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -144,7 +144,7 @@ function HeaderView({ } // Task is not closed - if (!ReportUtils.canWriteInReport(report) && report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { + if (ReportUtils.canWriteInReport(report) && report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) { threeDotMenuItems.push({ icon: Expensicons.Trashcan, text: translate('common.delete'), diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index 6a2ba10f837b..513b05f78525 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -915,7 +915,7 @@ function ReportActionItem({ originalReportID={originalReportID ?? ''} isArchivedRoom={ReportUtils.isArchivedRoom(report)} displayAsGroup={displayAsGroup} - disabledActions={ReportUtils.canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []} + disabledActions={!ReportUtils.canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []} isVisible={hovered && draftMessage === undefined && !hasErrors} draftMessage={draftMessage} isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index d6141d5152d9..b22f21c8df13 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -141,7 +141,7 @@ function ReportFooter({ /> )} {isArchivedRoom && <ArchivedReportFooter report={report} />} - {canWriteInReport && <OnboardingReportFooterMessage />} + {!canWriteInReport && <OnboardingReportFooterMessage />} {!isSmallScreenWidth && <View style={styles.offlineIndicatorRow}>{hideComposer && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}</View>} </View> )} From 3512f7c23991a8b33d0970d70676d94df6819635 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 11:11:54 +0200 Subject: [PATCH 455/580] fix report footer styles --- src/pages/home/report/ReportFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index b22f21c8df13..73ab8fcbac40 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -133,7 +133,7 @@ function ReportFooter({ return ( <> {hideComposer && ( - <View style={[styles.chatFooter, isArchivedRoom || isAnonymousUser ? styles.mt4 : {}, isSmallScreenWidth ? styles.mb5 : null]}> + <View style={[styles.chatFooter, isArchivedRoom || isAnonymousUser || !canWriteInReport ? styles.mt4 : {}, isSmallScreenWidth ? styles.mb5 : null]}> {isAnonymousUser && !isArchivedRoom && ( <AnonymousReportFooter report={report} From 69b603a9ba2cdf8a0efedc63a4fc5f44344957b1 Mon Sep 17 00:00:00 2001 From: dragnoir <mosaixel.org@gmail.com> Date: Fri, 26 Apr 2024 10:22:28 +0100 Subject: [PATCH 456/580] error to danger --- src/components/MoneyRequestHeader.tsx | 2 +- src/components/MoneyRequestHeaderStatusBar.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 85890bd9abd2..a9531fd9f9a3 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -213,7 +213,7 @@ function MoneyRequestHeader({ title={translate('iou.hold')} description={translate('iou.expenseOnHold')} shouldShowBorderBottom - error + danger /> )} </View> diff --git a/src/components/MoneyRequestHeaderStatusBar.tsx b/src/components/MoneyRequestHeaderStatusBar.tsx index cc0b1279204e..7f26b239dcc8 100644 --- a/src/components/MoneyRequestHeaderStatusBar.tsx +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -14,11 +14,11 @@ type MoneyRequestHeaderStatusBarProps = { /** Whether we show the border bottom */ shouldShowBorderBottom: boolean; - /** Is Error type */ - error?: boolean; + /** Whether we should use the danger theme color */ + danger?: boolean; }; -function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom, error = false}: MoneyRequestHeaderStatusBarProps) { +function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom, danger = false}: MoneyRequestHeaderStatusBarProps) { const styles = useThemeStyles(); const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; return ( @@ -27,7 +27,7 @@ function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom <Badge text={title} badgeStyles={styles.ml0} - error={error} + error={danger} /> </View> <View style={[styles.flexShrink1]}> From 75ccad0480d8ef62a2941401497464b7d12e3be5 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Fri, 26 Apr 2024 11:32:38 +0200 Subject: [PATCH 457/580] fix PR comments --- .../workspace/WorkspaceMoreFeaturesPage.tsx | 2 +- .../accounting/PolicyAccountingPage.tsx | 35 ++++++------------- 2 files changed, 12 insertions(+), 25 deletions(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index c3515c393123..cc724d00fe1d 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -102,7 +102,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro titleTranslationKey: 'workspace.moreFeatures.taxes.title', subtitleTranslationKey: 'workspace.moreFeatures.taxes.subtitle', isActive: (policy?.tax?.trackingEnabled ?? false) || isSyncTaxEnabled, - disabled: isSyncTaxEnabled || policy?.connections?.quickbooksOnline.data.country === CONST.COUNTRY.US, + disabled: isSyncTaxEnabled || policy?.connections?.quickbooksOnline?.data.country === CONST.COUNTRY.US, pendingAction: policy?.pendingFields?.tax, action: (isEnabled: boolean) => { Policy.enablePolicyTaxes(policy?.id ?? '', isEnabled); diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index a682351a038c..b88b6950c628 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -239,13 +239,17 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting ); return otherIntegrations.map((integration) => { const integrationData = accountingIntegrationData(integration, policyID, translate, true, connectedIntegration); + const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {}; return { - icon: integrationData?.icon, + ...iconProps, title: integrationData?.title, rightComponent: integrationData?.setupConnectionButton, + interactive: false, + shouldShowRightComponent: true, + wrapperStyle: styles.sectionMenuItemTopDescription, }; }); - }, [connectedIntegration, connectionSyncProgress?.connectionName, isSyncInProgress, policy?.connections, policyID, translate]); + }, [connectedIntegration, connectionSyncProgress?.connectionName, isSyncInProgress, policy?.connections, policyID, styles.sectionMenuItemTopDescription, translate]); const headerThreeDotsMenuItems: ThreeDotsMenuProps['menuItems'] = [ { @@ -296,33 +300,16 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting menuItems={connectionsMenuItems} shouldUseSingleExecution /> - {otherIntegrationsItems && otherIntegrationsItems?.length > 0 && ( + {otherIntegrationsItems && ( <CollapsibleSection title={translate('workspace.accounting.other')} wrapperStyle={styles.pr3} titleStyle={[styles.textNormal, styles.colorMuted]} > - {otherIntegrationsItems.map((integration) => - integration?.icon ? ( - <MenuItem - icon={integration?.icon} - iconType={CONST.ICON_TYPE_AVATAR} - interactive={false} - shouldShowRightComponent - wrapperStyle={styles.sectionMenuItemTopDescription} - title={integration.title} - rightComponent={integration.rightComponent} - /> - ) : ( - <MenuItem - interactive={false} - shouldShowRightComponent - wrapperStyle={styles.sectionMenuItemTopDescription} - title={integration.title} - rightComponent={integration.rightComponent} - /> - ), - )} + <MenuItemList + menuItems={otherIntegrationsItems} + shouldUseSingleExecution + /> </CollapsibleSection> )} </Section> From 6665e29bd15a917e12d4ac549fc229d4fd2e3747 Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Fri, 26 Apr 2024 11:33:48 +0200 Subject: [PATCH 458/580] Add fixes to Search widget after review --- src/ONYXKEYS.ts | 4 +- src/components/Search.tsx | 37 ++----------------- ...m.tsx => TemporaryTransactionListItem.tsx} | 4 +- src/languages/en.ts | 6 +-- src/libs/API/parameters/Search.ts | 6 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 3 +- src/libs/SearchUtils.ts | 30 ++++++++++----- src/libs/actions/Search.ts | 6 +-- src/types/onyx/index.ts | 4 +- 10 files changed, 45 insertions(+), 56 deletions(-) rename src/components/SelectionList/{TemporaryExpenseListItem.tsx => TemporaryTransactionListItem.tsx} (85%) create mode 100644 src/libs/API/parameters/Search.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 6f8e1c21284d..238ee2cec0b2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -358,7 +358,7 @@ const ONYXKEYS = { DEPRECATED_POLICY_MEMBER_LIST: 'policyMemberList_', // Search Page related - SEARCH: 'search_' + SNAPSHOT: 'snapshot_', }, /** List of Form ids */ @@ -563,7 +563,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; - [ONYXKEYS.COLLECTION.SEARCH]: OnyxTypes.SearchResults; + [ONYXKEYS.COLLECTION.SNAPSHOT]: OnyxTypes.SearchResults; }; type OnyxValuesMapping = { diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 611c1498e264..0fb0db1924a2 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -9,45 +9,14 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; -// For testing purposes run this code in browser console to insert fake data: -// query is the param from URL, by default it will be "all" -// Onyx.set(`search_${query}`, { -// search: { -// offset: 0, -// type: 'transaction', -// hasMoreResults: false, -// }, -// data: { -// transactions_1234: { -// receipt: {source: 'http...'}, -// hasEReceipt: false, -// created: '2024-04-11 00:00:00', -// amount: 12500, -// type: 'cash', -// reportID: '1', -// transactionThreadReportID: '2', -// transactionID: '1234', -// }, -// transactions_5555: { -// receipt: {source: 'http...'}, -// hasEReceipt: false, -// created: '2024-04-11 00:00:00', -// amount: 12500, -// type: 'cash', // not present in live data (data outside of snapshot_) -// reportID: '1', -// transactionThreadReportID: '2', -// transactionID: '5555', -// }, -// }, -// }) - type SearchProps = { query: string; }; function Search({query}: SearchProps) { const {isOffline} = useNetwork(); - const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SEARCH}${query}`); + const hash = SearchUtils.getQueryHash(query); + const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); useEffect(() => { SearchActions.search(query); @@ -67,7 +36,7 @@ function Search({query}: SearchProps) { const ListItem = SearchUtils.getListItem(); // This will be updated with the proper List component in another PR - return SearchUtils.getTransactionsSections(searchResults.data).map((item) => ( + return SearchUtils.getSections(searchResults.data).map((item) => ( <ListItem key={item.transactionID} item={item} diff --git a/src/components/SelectionList/TemporaryExpenseListItem.tsx b/src/components/SelectionList/TemporaryTransactionListItem.tsx similarity index 85% rename from src/components/SelectionList/TemporaryExpenseListItem.tsx rename to src/components/SelectionList/TemporaryTransactionListItem.tsx index 011a6d73ac4f..49544c4f5557 100644 --- a/src/components/SelectionList/TemporaryExpenseListItem.tsx +++ b/src/components/SelectionList/TemporaryTransactionListItem.tsx @@ -6,7 +6,7 @@ import type {SearchTransaction} from '@src/types/onyx/SearchResults'; // NOTE: This is a completely temporary mock item so that something can be displayed in SearchWidget // This should be removed and implement properly in: https://github.com/Expensify/App/issues/39877 -function ExpenseListItem({item}: {item: SearchTransaction}) { +function TransactionListItem({item}: {item: SearchTransaction}) { const styles = useThemeStyles(); return ( <View style={[styles.pt8]}> @@ -15,4 +15,4 @@ function ExpenseListItem({item}: {item: SearchTransaction}) { ); } -export default ExpenseListItem; +export default TransactionListItem; diff --git a/src/languages/en.ts b/src/languages/en.ts index 4bf45a608b48..e4ff69fc6859 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2453,11 +2453,11 @@ export default { search: { resultsAreLimited: 'Search results are limited.', searchResults: { - emptyResults:{ + emptyResults: { title: 'Nothing to show', subtitle: 'Try creating something using the green + button.', - } - } + }, + }, }, genericErrorPage: { title: 'Uh-oh, something went wrong!', diff --git a/src/libs/API/parameters/Search.ts b/src/libs/API/parameters/Search.ts new file mode 100644 index 000000000000..44e8c9f2d0fb --- /dev/null +++ b/src/libs/API/parameters/Search.ts @@ -0,0 +1,6 @@ +type SearchParams = { + query: string; + hash: number; +}; + +export default SearchParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 2d7948076548..71f6a5d5990a 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -215,3 +215,4 @@ export type {default as ShareTrackedExpenseParams} from './ShareTrackedExpensePa export type {default as CategorizeTrackedExpenseParams} from './CategorizeTrackedExpenseParams'; export type {default as LeavePolicyParams} from './LeavePolicyParams'; export type {default as OpenPolicyAccountingPageParams} from './OpenPolicyAccountingPageParams'; +export type {default as SearchParams} from './Search'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 5a4000ec6cbc..8bced2295475 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -465,7 +465,7 @@ const READ_COMMANDS = { OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage', OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage', - SEARCH: 'Search' + SEARCH: 'Search', } as const; type ReadCommand = ValueOf<typeof READ_COMMANDS>; @@ -509,6 +509,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; [READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE]: Parameters.OpenPolicyMoreFeaturesPageParams; [READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams; + [READ_COMMANDS.SEARCH]: Parameters.SearchParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index ccced12a0bc4..77185ccbac02 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,24 +1,36 @@ -import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; +import TransactionListItem from '@components/SelectionList/TemporaryTransactionListItem'; +import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; +import * as UserUtils from './UserUtils'; + +function getTransactionsSections(data: OnyxTypes.SearchResults['data']): SearchTransaction[] { + return Object.entries(data) + .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) + .map(([, value]) => value); +} const searchTypeToItemMap = { transaction: { - listItem: ExpenseListItem, + listItem: TransactionListItem, + getSections: getTransactionsSections, }, }; -const getTransactionsSections = (data: OnyxTypes.SearchResults['data']): SearchTransaction[] => - Object.entries(data) - .filter(([key]) => key.startsWith('transactions_')) - .map(([, value]) => value); - /** * TODO: in future make this function generic and return specific item component based on type * For now only 1 search item type exists in the app so this function is simplified */ -function getListItem(): typeof ExpenseListItem { +function getListItem(): typeof TransactionListItem { return searchTypeToItemMap.transaction.listItem; } -export {getTransactionsSections, getListItem}; +function getSections(data: OnyxTypes.SearchResults['data']): SearchTransaction[] { + return searchTypeToItemMap.transaction.getSections(data); +} + +function getQueryHash(query: string): number { + return UserUtils.hashText(query, 2 ** 32); +} + +export {getQueryHash, getListItem, getSections}; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 0165c5dccb34..4bb78d7c161d 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1,8 +1,8 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import * as UserUtils from '@libs/UserUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; import {READ_COMMANDS} from '@libs/API/types'; +import * as SearchUtils from '@libs/SearchUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; let isNetworkOffline = false; Onyx.connect({ @@ -17,7 +17,7 @@ function search(query: string) { return; } - const hash = UserUtils.hashText(query, 2 ** 32); + const hash = SearchUtils.getQueryHash(query); API.read(READ_COMMANDS.SEARCH, {query, hash}); } diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 2b4cf4c87584..1695daebace8 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -63,6 +63,7 @@ import type ReportUserIsTyping from './ReportUserIsTyping'; import type Request from './Request'; import type Response from './Response'; import type ScreenShareRequest from './ScreenShareRequest'; +import type SearchResults from './SearchResults'; import type SecurityGroup from './SecurityGroup'; import type SelectedTabRequest from './SelectedTabRequest'; import type Session from './Session'; @@ -80,7 +81,6 @@ import type WalletStatement from './WalletStatement'; import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; import type WorkspaceRateAndUnit from './WorkspaceRateAndUnit'; -import type SearchResults from './SearchResults' export type { Account, @@ -178,5 +178,5 @@ export type { Log, PolicyJoinMember, CapturedLogs, - SearchResults + SearchResults, }; From be992f14158f1e245d2330fc20b4af815dd6f355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= <contact@fabiohenriques.dev> Date: Fri, 26 Apr 2024 10:34:57 +0100 Subject: [PATCH 459/580] Remove `error` property from PlaidData --- src/CONST.ts | 1 - src/types/onyx/PlaidData.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0640d7686d19..c2612a8a6695 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1379,7 +1379,6 @@ const CONST = { plaidAccessToken: '', bankAccounts: [] as PlaidBankAccount[], isLoading: false, - error: '', errors: {}, }, }, diff --git a/src/types/onyx/PlaidData.ts b/src/types/onyx/PlaidData.ts index 3a538efe3623..8ec93119cbd8 100644 --- a/src/types/onyx/PlaidData.ts +++ b/src/types/onyx/PlaidData.ts @@ -15,7 +15,6 @@ type PlaidData = { bankAccounts?: PlaidBankAccount[]; isLoading?: boolean; - error?: string; errors: OnyxCommon.Errors; }; From baae10ecbb15b2416d4ef53ac6e709f327fa8092 Mon Sep 17 00:00:00 2001 From: nkdengineer <nkdengineer@outlook.com> Date: Fri, 26 Apr 2024 16:35:17 +0700 Subject: [PATCH 460/580] Hide new paid by item --- src/components/MoneyRequestConfirmationList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index d5cc00bce1cc..14e6f636ebae 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -994,7 +994,8 @@ function MoneyRequestConfirmationList({ return ( <> - {isTypeSplit && action === CONST.IOU.ACTION.CREATE && ( + {/** Hide it temporarily, it will back when https://github.com/Expensify/App/pull/40386 is merged */} + {isTypeSplit && action === CONST.IOU.ACTION.CREATE && false && ( <MenuItem key={translate('moneyRequestConfirmationList.paidBy')} label={translate('moneyRequestConfirmationList.paidBy')} From 4e1d347ecc3f628eb87324b145b704649bf63f42 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Fri, 26 Apr 2024 11:45:14 +0200 Subject: [PATCH 461/580] fix linter --- src/pages/workspace/accounting/PolicyAccountingPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index b88b6950c628..53727d6a8502 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -11,7 +11,6 @@ import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import type {MenuItemProps} from '@components/MenuItem'; -import MenuItem from '@components/MenuItem'; import MenuItemList from '@components/MenuItemList'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; From 43c0ad72d8eb403401951c2f48f9c0b2500f7107 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 26 Apr 2024 12:22:16 +0200 Subject: [PATCH 462/580] Don't show three dots on the confirmation screen --- src/pages/iou/request/step/IOURequestStepConfirmation.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 972adeec1d69..e360b4c38717 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -512,9 +512,7 @@ function IOURequestStepConfirmation({ <HeaderWithBackButton title={headerTitle} onBackButtonPress={navigateBack} - shouldShowThreeDotsButton={ - requestType === CONST.IOU.REQUEST_TYPE.MANUAL && (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.TRACK || iouType === CONST.IOU.TYPE.INVOICE) - } + shouldShowThreeDotsButton={requestType === CONST.IOU.REQUEST_TYPE.MANUAL && (iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.TRACK)} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} threeDotsMenuItems={[ { From ef395d2d4b351c0cb56e34500ad14ae3a6a026f9 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano <rocio@expensify.com> Date: Fri, 26 Apr 2024 12:30:28 +0200 Subject: [PATCH 463/580] Prettier --- config/webpack/webpack.dev.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/webpack/webpack.dev.ts b/config/webpack/webpack.dev.ts index 4a004fb61d6d..7a196da6b691 100644 --- a/config/webpack/webpack.dev.ts +++ b/config/webpack/webpack.dev.ts @@ -23,7 +23,7 @@ const getConfiguration = (environment: Environment): Promise<Configuration> => : { proxy: [ { - context: ["/api", "/staging", "/chat-attachments", "/receipts"], + context: ['/api', '/staging', '/chat-attachments', '/receipts'], target: 'http://[::1]:9000', }, ], From 1e8053ed65da560ee78ae19b61442674bbf5db6e Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Fri, 26 Apr 2024 10:53:33 +0000 Subject: [PATCH 464/580] Update version to 1.4.66-3 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index dca58eeb96ca..7d3048bc8eb8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046602 - versionName "1.4.66-2" + versionCode 1001046603 + versionName "1.4.66-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index cda215581cff..6208f4645762 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.66.2</string> + <string>1.4.66.3</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 43728a764228..28efc0914959 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.66.2</string> + <string>1.4.66.3</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 5d52107c99bf..1ef215d43912 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.66</string> <key>CFBundleVersion</key> - <string>1.4.66.2</string> + <string>1.4.66.3</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index 9c645b6cbe94..989a629caf0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.66-2", + "version": "1.4.66-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.66-2", + "version": "1.4.66-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2860b3c1d3a9..751108d025fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.66-2", + "version": "1.4.66-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 0c80ad7552b9e025cf6ae9c5caae0531a0d244fb Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 13:02:17 +0200 Subject: [PATCH 465/580] prettify --- src/CONST.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 7d10f42de7a7..b09d3f173afd 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -846,7 +846,7 @@ const CONST = { WRITE: 'write', SHARE: 'share', OWN: 'own', - } + }, }, NEXT_STEP: { FINISHED: 'Finished!', From fa29a354df9be559c2e5008523562c7384fb2aeb Mon Sep 17 00:00:00 2001 From: Vit Horacek <36083550+mountiny@users.noreply.github.com> Date: Fri, 26 Apr 2024 13:17:30 +0200 Subject: [PATCH 466/580] Revert "Use fallback user avatar in cases where the user is unknown to us" --- src/components/Avatar.tsx | 14 ++----- src/components/AvatarWithIndicator.tsx | 9 ++-- src/components/MultipleAvatars.tsx | 2 - src/components/ReportActionItem/TaskView.tsx | 2 +- .../BaseUserDetailsTooltip/index.tsx | 4 +- src/libs/OptionsListUtils.ts | 37 ++++++++++------ src/libs/PersonalDetailsUtils.ts | 1 + src/libs/ReportUtils.ts | 42 +++++++++---------- src/libs/SidebarUtils.ts | 3 +- src/libs/UserUtils.ts | 22 ++++++---- src/libs/actions/IOU.ts | 5 +++ src/libs/actions/Report.ts | 2 + src/libs/actions/Task.ts | 3 +- src/pages/DetailsPage.tsx | 4 +- src/pages/ProfilePage.tsx | 6 ++- src/pages/ReportParticipantDetailsPage.tsx | 6 +-- src/pages/ReportParticipantsPage.tsx | 4 +- src/pages/RoomMembersPage.tsx | 4 +- .../report/ReactionList/BaseReactionList.tsx | 4 +- .../ReportActionCompose/SuggestionMention.tsx | 5 +-- .../home/report/ReportActionItemSingle.tsx | 17 +++----- .../sidebar/ProfileAvatarWithIndicator.tsx | 4 +- src/pages/workspace/WorkspaceMembersPage.tsx | 4 +- .../members/WorkspaceMemberDetailsPage.tsx | 5 ++- .../WorkspaceWorkflowsApproverPage.tsx | 4 +- .../workflows/WorkspaceWorkflowsPayerPage.tsx | 4 +- src/types/onyx/OnyxCommon.ts | 2 +- 27 files changed, 117 insertions(+), 102 deletions(-) diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 99183a1e6ba7..bf48894beaab 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -7,7 +7,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; import type {AvatarSource} from '@libs/UserUtils'; -import * as UserUtils from '@libs/UserUtils'; import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; import type {AvatarType} from '@src/types/onyx/OnyxCommon'; @@ -50,13 +49,10 @@ type AvatarProps = { /** Owner of the avatar. If user, displayName. If workspace, policy name */ name?: string; - - /** Optional account id if it's user avatar */ - accountID?: number; }; function Avatar({ - source: originalSource, + source, imageStyles, iconAdditionalStyles, containerStyles, @@ -66,7 +62,6 @@ function Avatar({ fallbackIconTestID = '', type = CONST.ICON_TYPE_AVATAR, name = '', - accountID, }: AvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -77,17 +72,16 @@ function Avatar({ useEffect(() => { setImageError(false); - }, [originalSource]); + }, [source]); const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; - const iconSize = StyleUtils.getAvatarSize(size); + const imageStyle: StyleProp<ImageStyle> = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; // We pass the color styles down to the SVG for the workspace and fallback avatar. - const source = isWorkspace ? originalSource : UserUtils.getAvatar(originalSource, accountID); - const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar; + const useFallBackAvatar = imageError || source === Expensicons.FallbackAvatar || !source; const fallbackAvatar = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar; const fallbackAvatarTestID = isWorkspace ? ReportUtils.getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon'; const avatarSource = useFallBackAvatar ? fallbackAvatar : source; diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index 1bf18afb70ff..42b91b3d2d71 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -11,10 +11,7 @@ import Tooltip from './Tooltip'; type AvatarWithIndicatorProps = { /** URL for the avatar */ - source?: UserUtils.AvatarSource; - - /** account id if it's user avatar */ - accountID?: number; + source: UserUtils.AvatarSource; /** To show a tooltip on hover */ tooltipText?: string; @@ -26,7 +23,7 @@ type AvatarWithIndicatorProps = { isLoading?: boolean; }; -function AvatarWithIndicator({source, accountID, tooltipText = '', fallbackIcon = Expensicons.FallbackAvatar, isLoading = true}: AvatarWithIndicatorProps) { +function AvatarWithIndicator({source, tooltipText = '', fallbackIcon = Expensicons.FallbackAvatar, isLoading = true}: AvatarWithIndicatorProps) { const styles = useThemeStyles(); return ( @@ -38,7 +35,7 @@ function AvatarWithIndicator({source, accountID, tooltipText = '', fallbackIcon <> <Avatar size={CONST.AVATAR_SIZE.SMALL} - source={UserUtils.getSmallSizeAvatar(source, accountID)} + source={UserUtils.getSmallSizeAvatar(source)} fallbackIcon={fallbackIcon} /> <Indicator /> diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 31d3d35af58d..dedaba500a9c 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -158,7 +158,6 @@ function MultipleAvatars({ name={icons[0].name} type={icons[0].type} fallbackIcon={icons[0].fallbackIcon} - accountID={icons[0].id} /> </View> </UserDetailsTooltip> @@ -208,7 +207,6 @@ function MultipleAvatars({ name={icon.name} type={icon.type} fallbackIcon={icon.fallbackIcon} - accountID={icon.id} /> </View> </UserDetailsTooltip> diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index e3e07ab0d7ad..9711e126907f 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -156,7 +156,7 @@ function TaskView({report, shouldShowHorizontalRule, ...props}: TaskViewProps) { <MenuItem label={translate('task.assignee')} title={ReportUtils.getDisplayNameForParticipant(report.managerID)} - icon={OptionsListUtils.getAvatarsForAccountIDs([report.managerID], personalDetails)} + icon={OptionsListUtils.getAvatarsForAccountIDs(report.managerID ? [report.managerID] : [], personalDetails)} iconType={CONST.ICON_TYPE_AVATAR} avatarSize={CONST.AVATAR_SIZE.SMALLER} titleStyle={styles.assigneeTextStyle} diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx index 12c4928616f0..592cec3beca5 100644 --- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx +++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx @@ -10,6 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateAccountID, shiftHorizontal, children}: UserDetailsTooltipProps) { @@ -54,11 +55,10 @@ function BaseUserDetailsTooltip({accountID, fallbackUserDetails, icon, delegateA <View style={styles.emptyAvatar}> <Avatar containerStyles={[styles.actionAvatar]} - source={icon?.source ?? userAvatar} + source={icon?.source ?? UserUtils.getAvatar(userAvatar, userAccountID)} type={icon?.type ?? CONST.ICON_TYPE_AVATAR} name={icon?.name ?? userLogin} fallbackIcon={icon?.fallbackIcon} - accountID={userAccountID} /> </View> <Text style={[styles.mt2, styles.textMicroBold, styles.textReactionSenders, styles.textAlignCenter]}>{title}</Text> diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 46bc5f90ec04..8166036e8e17 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -7,7 +7,6 @@ import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import type {SelectedTagOption} from '@components/TagPicker'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -305,14 +304,13 @@ function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: OnyxEntr Object.entries(defaultValues).forEach((item) => { reversedDefaultValues[item[1]] = item[0]; }); - return accountIDs.map((accountID) => { const login = reversedDefaultValues[accountID] ?? ''; - const userPersonalDetail = personalDetails?.[accountID] ?? {login, accountID}; + const userPersonalDetail = personalDetails?.[accountID] ?? {login, accountID, avatar: ''}; return { id: accountID, - source: userPersonalDetail.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(userPersonalDetail.avatar, userPersonalDetail.accountID), type: CONST.ICON_TYPE_AVATAR, name: userPersonalDetail.login ?? '', }; @@ -335,7 +333,9 @@ function getPersonalDetailsForAccountIDs(accountIDs: number[] | undefined, perso } let personalDetail: OnyxEntry<PersonalDetails> = personalDetails[accountID]; if (!personalDetail) { - personalDetail = {} as PersonalDetails; + personalDetail = { + avatar: UserUtils.getDefaultAvatar(cleanAccountID), + } as PersonalDetails; } if (cleanAccountID === CONST.ACCOUNT_ID.CONCIERGE) { @@ -364,7 +364,6 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const login = detail?.login || participant.login || ''; const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login)); - return { keyForList: String(detail?.accountID), login, @@ -375,7 +374,7 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName, icons: [ { - source: detail?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(detail?.avatar ?? '', detail?.accountID ?? -1), name: login, type: CONST.ICON_TYPE_AVATAR, id: detail?.accountID, @@ -742,7 +741,13 @@ function createOption( // Disabling this line for safeness as nullish coalescing works only if the value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing result.searchText = getSearchText(report, reportName, personalDetailList, !!result.isChatRoom || !!result.isPolicyExpenseChat, !!result.isThread); - result.icons = ReportUtils.getIcons(report, personalDetails, personalDetail?.avatar, personalDetail?.login, personalDetail?.accountID); + result.icons = ReportUtils.getIcons( + report, + personalDetails, + UserUtils.getAvatar(personalDetail?.avatar ?? '', personalDetail?.accountID), + personalDetail?.login, + personalDetail?.accountID, + ); result.subtitle = subtitle; return result; @@ -1843,6 +1848,7 @@ function getOptions( [optimisticAccountID]: { accountID: optimisticAccountID, login: searchValue, + avatar: UserUtils.getDefaultAvatar(optimisticAccountID), }, }; userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, reportActions, { @@ -1855,10 +1861,10 @@ function getOptions( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing userToInvite.alternateText = userToInvite.alternateText || searchValue; - // If user doesn't exist, use a fallback avatar + // If user doesn't exist, use a default avatar userToInvite.icons = [ { - source: FallbackAvatar, + source: UserUtils.getAvatar('', optimisticAccountID), name: searchValue, type: CONST.ICON_TYPE_AVATAR, }, @@ -1932,12 +1938,17 @@ function getShareLogOptions(options: OptionList, searchValue = '', betas: Beta[] */ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails | EmptyObject, amountText?: string): PayeePersonalDetails { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); - const icons = [{source: personalDetail.avatar ?? FallbackAvatar, name: personalDetail.login ?? '', type: CONST.ICON_TYPE_AVATAR, id: personalDetail.accountID}]; - return { text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin), alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false), - icons, + icons: [ + { + source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), + name: personalDetail.login ?? '', + type: CONST.ICON_TYPE_AVATAR, + id: personalDetail.accountID, + }, + ], descriptiveText: amountText ?? '', login: personalDetail.login ?? '', accountID: personalDetail.accountID, diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 961b3db13487..ffa0605f1eba 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -153,6 +153,7 @@ function getPersonalDetailsOnyxDataForOptimisticUsers(newLogins: string[], newAc personalDetailsNew[accountID] = { login, accountID, + avatar: UserUtils.getDefaultAvatarURL(accountID), displayName: LocalePhoneNumber.formatPhoneNumber(login), }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 121518130cb4..ca5080cc4afc 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -10,7 +10,7 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; +import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {IOUAction, IOUType} from '@src/CONST'; @@ -1631,7 +1631,7 @@ function getIconsForParticipants(participants: number[], personalDetails: OnyxCo const participantsList = participants || []; for (const accountID of participantsList) { - const avatarSource = personalDetails?.[accountID]?.avatar ?? FallbackAvatar; + const avatarSource = UserUtils.getAvatar(personalDetails?.[accountID]?.avatar ?? '', accountID); const displayNameLogin = personalDetails?.[accountID]?.displayName ? personalDetails?.[accountID]?.displayName : personalDetails?.[accountID]?.login; participantDetails.push([accountID, displayNameLogin ?? '', avatarSource, personalDetails?.[accountID]?.fallbackIcon ?? '']); } @@ -1692,12 +1692,12 @@ function getPersonalDetailsForAccountID(accountID: number): Partial<PersonalDeta if (!accountID) { return {}; } - - const defaultDetails = { - isOptimisticPersonalDetail: true, - }; - - return allPersonalDetails?.[accountID] ?? defaultDetails; + return ( + allPersonalDetails?.[accountID] ?? { + avatar: UserUtils.getDefaultAvatar(accountID), + isOptimisticPersonalDetail: true, + } + ); } /** @@ -1819,7 +1819,7 @@ function getIcons( ): Icon[] { if (isEmptyObject(report)) { const fallbackIcon: Icon = { - source: defaultIcon ?? FallbackAvatar, + source: defaultIcon ?? Expensicons.FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: defaultName, id: defaultAccountID, @@ -1830,7 +1830,7 @@ function getIcons( const parentReportAction = ReportActionsUtils.getParentReportAction(report); const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { - source: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(personalDetails?.[parentReportAction.actorAccountID ?? -1]?.avatar ?? '', parentReportAction.actorAccountID ?? -1), id: parentReportAction.actorAccountID, type: CONST.ICON_TYPE_AVATAR, name: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.displayName ?? '', @@ -1846,7 +1846,7 @@ function getIcons( const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1], '', false); const actorIcon = { id: actorAccountID, - source: personalDetails?.[actorAccountID ?? -1]?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(personalDetails?.[actorAccountID ?? -1]?.avatar ?? '', actorAccountID ?? -1), name: actorDisplayName, type: CONST.ICON_TYPE_AVATAR, fallbackIcon: personalDetails?.[parentReportAction.actorAccountID ?? -1]?.fallbackIcon, @@ -1861,7 +1861,7 @@ function getIcons( if (isTaskReport(report)) { const ownerIcon = { id: report?.ownerAccountID, - source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), type: CONST.ICON_TYPE_AVATAR, name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, @@ -1893,7 +1893,7 @@ function getIcons( if (isPolicyExpenseChat(report) || isExpenseReport(report)) { const workspaceIcon = getWorkspaceIcon(report, policy); const memberIcon = { - source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), id: report?.ownerAccountID, type: CONST.ICON_TYPE_AVATAR, name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', @@ -1903,7 +1903,7 @@ function getIcons( } if (isIOUReport(report)) { const managerIcon = { - source: personalDetails?.[report?.managerID ?? -1]?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(personalDetails?.[report?.managerID ?? -1]?.avatar ?? '', report?.managerID ?? -1), id: report?.managerID, type: CONST.ICON_TYPE_AVATAR, name: personalDetails?.[report?.managerID ?? -1]?.displayName ?? '', @@ -1911,7 +1911,7 @@ function getIcons( }; const ownerIcon = { id: report?.ownerAccountID, - source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? '', report?.ownerAccountID ?? -1), type: CONST.ICON_TYPE_AVATAR, name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '', fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon, @@ -1957,7 +1957,7 @@ function getDisplayNamesWithTooltips( const accountID = Number(user?.accountID); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const displayName = getDisplayNameForParticipant(accountID, isMultipleParticipantReport, shouldFallbackToHidden, shouldAddCurrentUserPostfix) || user?.login || ''; - const avatar = user && 'avatar' in user ? user.avatar : FallbackAvatar; + const avatar = UserUtils.getDefaultAvatar(accountID); let pronouns = user?.pronouns ?? undefined; if (pronouns?.startsWith(CONST.PRONOUNS.PREFIX)) { @@ -3243,7 +3243,7 @@ function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, }, ], automatic: false, - avatar: allPersonalDetails?.[accountID ?? -1]?.avatar, + avatar: allPersonalDetails?.[accountID ?? -1]?.avatar ?? UserUtils.getDefaultAvatarURL(accountID), created: DateUtils.getDBTimeWithSkew(Date.now() + createdOffset), message: [ { @@ -6057,10 +6057,10 @@ function hasMissingPaymentMethod(userWallet: OnyxEntry<UserWallet>, iouReportID: /** * Used from expense actions to decide if we need to build an optimistic expense report. - Create a new report if: - - we don't have an iouReport set in the chatReport - - we have one, but it's waiting on the payee adding a bank account - - we have one but we can't add more transactions to it due to: report is approved or settled, or report is processing and policy isn't on Instant submit reporting frequency + Create a new report if: + - we don't have an iouReport set in the chatReport + - we have one, but it's waiting on the payee adding a bank account + - we have one but we can't add more transactions to it due to: report is approved or settled, or report is processing and policy isn't on Instant submit reporting frequency */ function shouldCreateNewMoneyRequestReport(existingIOUReport: OnyxEntry<Report> | undefined | null, chatReport: OnyxEntry<Report> | null): boolean { return !existingIOUReport || hasIOUWaitingOnCurrentUserBankAccount(chatReport) || !canAddOrDeleteTransactions(existingIOUReport); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index d230f58e46f9..248878df2bf8 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -20,6 +20,7 @@ import * as OptionsListUtils from './OptionsListUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; +import * as UserUtils from './UserUtils'; const visibleReportActionItems: ReportActions = {}; Onyx.connect({ @@ -414,7 +415,7 @@ function getOptionData({ result.subtitle = subtitle; result.participantsList = participantPersonalDetailList; - result.icons = ReportUtils.getIcons(report, personalDetails, personalDetail?.avatar, '', -1, policy); + result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail?.avatar ?? {}, personalDetail?.accountID), '', -1, policy); result.searchText = OptionsListUtils.getSearchText(report, reportName, participantPersonalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); result.displayNamesWithTooltips = displayNamesWithTooltips; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 03066b21fa71..ce7e4963afc7 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import * as defaultAvatars from '@components/Icon/DefaultAvatars'; -import {ConciergeAvatar, NotificationsAvatar} from '@components/Icon/Expensicons'; +import {ConciergeAvatar, FallbackAvatar, NotificationsAvatar} from '@components/Icon/Expensicons'; import CONST from '@src/CONST'; import type {LoginList} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; @@ -82,7 +82,10 @@ function generateAccountID(searchValue: string): number { * @param [accountID] * @returns */ -function getDefaultAvatar(accountID = -1, avatarURL?: string): IconAsset | undefined { +function getDefaultAvatar(accountID = -1, avatarURL?: string): IconAsset { + if (accountID <= 0) { + return FallbackAvatar; + } if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) { return ConciergeAvatar; } @@ -122,7 +125,7 @@ function getDefaultAvatarURL(accountID: string | number = ''): string { } /** - * Given a user's avatar path, returns true if URL points to a default avatar, false otherwise + * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar * @param avatarSource - the avatar source from user's personalDetails */ function isDefaultAvatar(avatarSource?: AvatarSource): avatarSource is string | undefined { @@ -137,6 +140,11 @@ function isDefaultAvatar(avatarSource?: AvatarSource): avatarSource is string | } } + if (!avatarSource) { + // If source is undefined, we should also use a default avatar + return true; + } + return false; } @@ -147,7 +155,7 @@ function isDefaultAvatar(avatarSource?: AvatarSource): avatarSource is string | * @param avatarSource - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ -function getAvatar(avatarSource?: AvatarSource, accountID?: number): AvatarSource | undefined { +function getAvatar(avatarSource?: AvatarSource, accountID?: number): AvatarSource { return isDefaultAvatar(avatarSource) ? getDefaultAvatar(accountID, avatarSource) : avatarSource; } @@ -155,7 +163,7 @@ function getAvatar(avatarSource?: AvatarSource, accountID?: number): AvatarSourc * Provided an avatar URL, if avatar is a default avatar, return NewDot default avatar URL. * Otherwise, return the URL pointing to a user-uploaded avatar. * - * @param avatarSource - the avatar source from user's personalDetails + * @param avatarURL - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ function getAvatarUrl(avatarSource: AvatarSource | undefined, accountID: number): AvatarSource { @@ -166,7 +174,7 @@ function getAvatarUrl(avatarSource: AvatarSource | undefined, accountID: number) * Avatars uploaded by users will have a _128 appended so that the asset server returns a small version. * This removes that part of the URL so the full version of the image can load. */ -function getFullSizeAvatar(avatarSource: AvatarSource | undefined, accountID?: number): AvatarSource | undefined { +function getFullSizeAvatar(avatarSource: AvatarSource | undefined, accountID?: number): AvatarSource { const source = getAvatar(avatarSource, accountID); if (typeof source !== 'string') { return source; @@ -178,7 +186,7 @@ function getFullSizeAvatar(avatarSource: AvatarSource | undefined, accountID?: n * Small sized avatars end with _128.<file-type>. This adds the _128 at the end of the * source URL (before the file type) if it doesn't exist there already. */ -function getSmallSizeAvatar(avatarSource?: AvatarSource, accountID?: number): AvatarSource | undefined { +function getSmallSizeAvatar(avatarSource: AvatarSource, accountID?: number): AvatarSource { const source = getAvatar(avatarSource, accountID); if (typeof source !== 'string') { return source; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 98d5b83c332e..65e2e4f35c4a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -42,6 +42,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {OptimisticChatReport, OptimisticCreatedReportAction, OptimisticIOUReportAction, TransactionDetails} from '@libs/ReportUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import * as UserUtils from '@libs/UserUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; @@ -1438,6 +1439,7 @@ function getMoneyRequestInformation( ? { [payerAccountID]: { accountID: payerAccountID, + avatar: UserUtils.getDefaultAvatarURL(payerAccountID), // Disabling this line since participant.displayName can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing displayName: LocalePhoneNumber.formatPhoneNumber(participant.displayName || payerEmail), @@ -3391,6 +3393,7 @@ function createSplitsAndOnyxData( ? { [accountID]: { accountID, + avatar: UserUtils.getDefaultAvatarURL(accountID), // Disabling this line since participant.displayName can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing displayName: LocalePhoneNumber.formatPhoneNumber(participant.displayName || email), @@ -3832,6 +3835,7 @@ function startSplitBill({ value: { [accountID]: { accountID, + avatar: UserUtils.getDefaultAvatarURL(accountID), // Disabling this line since participant.displayName can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing displayName: LocalePhoneNumber.formatPhoneNumber(participant.displayName || email), @@ -5015,6 +5019,7 @@ function getSendMoneyParams( value: { [recipientAccountID]: { accountID: recipientAccountID, + avatar: UserUtils.getDefaultAvatarURL(recipient.accountID), // Disabling this line since participant.displayName can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing displayName: recipient.displayName || recipient.login, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7542ca12c592..eba477878135 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -71,6 +71,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import {doesReportBelongToWorkspace} from '@libs/ReportUtils'; import type {OptimisticAddCommentReportAction} from '@libs/ReportUtils'; import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation'; +import * as UserUtils from '@libs/UserUtils'; import Visibility from '@libs/Visibility'; import CONFIG from '@src/CONFIG'; import type {OnboardingPurposeType} from '@src/CONST'; @@ -820,6 +821,7 @@ function openReport( optimisticPersonalDetails[accountID] = allPersonalDetails?.[accountID] ?? { login, accountID, + avatar: UserUtils.getDefaultAvatarURL(accountID), displayName: login, isOptimisticPersonalDetail: true, }; diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index f31bffe46c2b..bfab83d9f146 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -14,6 +14,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; +import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -684,7 +685,7 @@ function setAssigneeValue( // If this is an optimistic report, we likely don't have their personal details yet so we set it here optimistically as well const optimisticPersonalDetailsListAction = { accountID: assigneeAccountID, - avatar: allPersonalDetails?.[assigneeAccountID]?.avatar, + avatar: allPersonalDetails?.[assigneeAccountID]?.avatar ?? UserUtils.getDefaultAvatarURL(assigneeAccountID), displayName: allPersonalDetails?.[assigneeAccountID]?.displayName ?? assigneeEmail, login: assigneeEmail, }; diff --git a/src/pages/DetailsPage.tsx b/src/pages/DetailsPage.tsx index 338e51cb408e..49b3e856c65d 100755 --- a/src/pages/DetailsPage.tsx +++ b/src/pages/DetailsPage.tsx @@ -70,6 +70,7 @@ function DetailsPage({personalDetails, route, session}: DetailsPageProps) { accountID: optimisticAccountID, login, displayName: login, + avatar: UserUtils.getDefaultAvatar(optimisticAccountID), }; } @@ -114,10 +115,9 @@ function DetailsPage({personalDetails, route, session}: DetailsPageProps) { <Avatar containerStyles={[styles.avatarLarge, styles.mb3]} imageStyles={[styles.avatarLarge]} - source={details?.avatar} + source={UserUtils.getAvatar(details?.avatar, details?.accountID)} size={CONST.AVATAR_SIZE.LARGE} fallbackIcon={details?.fallbackIcon} - accountID={details?.accountID} /> </OfflineWithFeedback> </PressableWithoutFocus> diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 310d77cf8391..a8e4223c0180 100755 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -25,6 +25,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {ProfileNavigatorParamList} from '@navigation/types'; import * as PersonalDetailsActions from '@userActions/PersonalDetails'; @@ -95,6 +96,8 @@ function ProfilePage({route}: ProfilePageProps) { const details: PersonalDetails | EmptyObject = personalDetails?.[accountID] ?? (ValidationUtils.isValidAccountRoute(accountID) ? {} : {accountID: 0, avatar: ''}); const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details, undefined, undefined, isCurrentUser); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const avatar = details?.avatar || UserUtils.getDefaultAvatar(); // we can have an empty string and in this case, we need to show the default avatar const fallbackIcon = details?.fallbackIcon ?? ''; const login = details?.login ?? ''; const timezone = details?.timezone; @@ -161,10 +164,9 @@ function ProfilePage({route}: ProfilePageProps) { <Avatar containerStyles={[styles.avatarXLarge, styles.mb3]} imageStyles={[styles.avatarXLarge]} - source={details.avatar} + source={UserUtils.getAvatar(avatar, accountID)} size={CONST.AVATAR_SIZE.XLARGE} fallbackIcon={fallbackIcon} - accountID={accountID} /> </OfflineWithFeedback> </PressableWithoutFocus> diff --git a/src/pages/ReportParticipantDetailsPage.tsx b/src/pages/ReportParticipantDetailsPage.tsx index 563f24635759..2b1411641faa 100644 --- a/src/pages/ReportParticipantDetailsPage.tsx +++ b/src/pages/ReportParticipantDetailsPage.tsx @@ -18,6 +18,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Report from '@libs/actions/Report'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import Navigation from '@navigation/Navigation'; import type {ParticipantsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; @@ -50,7 +51,7 @@ function ReportParticipantDetails({personalDetails, report, route}: ReportPartic const member = report?.participants?.[accountID]; const details = personalDetails?.[accountID] ?? ({} as PersonalDetails); - const avatar = details.avatar; + const avatar = details.avatar ?? UserUtils.getDefaultAvatar(); const fallbackIcon = details.fallbackIcon ?? ''; const displayName = details.displayName ?? ''; const isCurrentUserAdmin = ReportUtils.isGroupChatAdmin(report, currentUserPersonalDetails?.accountID); @@ -80,8 +81,7 @@ function ReportParticipantDetails({personalDetails, report, route}: ReportPartic <Avatar containerStyles={[styles.avatarXLarge, styles.mv5, styles.noOutline]} imageStyles={[styles.avatarXLarge]} - source={avatar} - accountID={accountID} + source={UserUtils.getAvatar(avatar, accountID)} size={CONST.AVATAR_SIZE.XLARGE} fallbackIcon={fallbackIcon} /> diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 995dc3045bda..9c0e19e85ee4 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -13,7 +13,6 @@ import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/Bu import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import TableListItem from '@components/SelectionList/TableListItem'; @@ -28,6 +27,7 @@ import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -99,7 +99,7 @@ function ReportParticipantsPage({report, personalDetails, session}: ReportPartic pendingAction: pendingChatMember?.pendingAction, icons: [ { - source: details.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(details?.avatar, accountID), name: formatPhoneNumber(details?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: accountID, diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index a301767f9fe6..67b6d96f182d 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -8,7 +8,6 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -27,6 +26,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -209,7 +209,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { alternateText: details?.login ? formatPhoneNumber(details.login) : '', icons: [ { - source: details.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(details.avatar, accountID), name: details.login ?? '', type: CONST.ICON_TYPE_AVATAR, id: Number(accountID), diff --git a/src/pages/home/report/ReactionList/BaseReactionList.tsx b/src/pages/home/report/ReactionList/BaseReactionList.tsx index 23417c1395df..6f56f8f09632 100755 --- a/src/pages/home/report/ReactionList/BaseReactionList.tsx +++ b/src/pages/home/report/ReactionList/BaseReactionList.tsx @@ -2,11 +2,11 @@ import Str from 'expensify-common/lib/str'; import React from 'react'; import {FlatList} from 'react-native'; import type {FlatListProps} from 'react-native'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import OptionRow from '@components/OptionRow'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; +import * as UserUtils from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -71,7 +71,7 @@ function BaseReactionList({hasUserReacted = false, users, isVisible = false, emo icons: [ { id: item.accountID, - source: item.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(item.avatar, item.accountID), name: item.login ?? '', type: CONST.ICON_TYPE_AVATAR, }, diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index f6c7d91b4efd..05e1163da200 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -5,7 +5,6 @@ import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, import {useOnyx} from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import type {Mention} from '@components/MentionSuggestions'; import MentionSuggestions from '@components/MentionSuggestions'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -17,6 +16,7 @@ import * as LoginUtils from '@libs/LoginUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; +import * as UserUtils from '@libs/UserUtils'; import {isValidRoomName} from '@libs/ValidationUtils'; import * as ReportUserActions from '@userActions/Report'; import CONST from '@src/CONST'; @@ -242,10 +242,9 @@ function SuggestionMention( icons: [ { name: detail?.login, - source: detail?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(detail?.avatar, detail?.accountID), type: CONST.ICON_TYPE_AVATAR, fallbackIcon: detail?.fallbackIcon, - id: detail?.accountID, }, ], }); diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 54b6775cfe13..dda17e1e83d3 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -3,7 +3,6 @@ import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Avatar from '@components/Avatar'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -20,6 +19,7 @@ import ControlSelection from '@libs/ControlSelection'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Report, ReportAction} from '@src/types/onyx'; @@ -86,14 +86,12 @@ function ReportActionItemSingle({ let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); const displayAllActors = useMemo(() => action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && iouReport, [action?.actionName, iouReport]); const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors); - let avatarSource = avatar; - let avatarAccountId = actorAccountID; + let avatarSource = UserUtils.getAvatar(avatar ?? '', actorAccountID); if (isWorkspaceActor) { displayName = ReportUtils.getPolicyName(report); actorHint = displayName; avatarSource = ReportUtils.getWorkspaceAvatar(report); - avatarAccountId = undefined; } else if (action?.delegateAccountID && personalDetails[action?.delegateAccountID]) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. @@ -101,8 +99,7 @@ function ReportActionItemSingle({ const delegateDisplayName = delegateDetails?.displayName; actorHint = `${delegateDisplayName} (${translate('reportAction.asCopilot')} ${displayName})`; displayName = actorHint; - avatarSource = delegateDetails?.avatar; - avatarAccountId = action.delegateAccountID; + avatarSource = UserUtils.getAvatar(delegateDetails?.avatar ?? '', Number(action.delegateAccountID)); } // If this is a report preview, display names and avatars of both people involved @@ -115,7 +112,7 @@ function ReportActionItemSingle({ const secondaryDisplayName = ReportUtils.getDisplayNameForParticipant(secondaryAccountId); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; secondaryAvatar = { - source: secondaryUserAvatar, + source: UserUtils.getAvatar(secondaryUserAvatar, secondaryAccountId), type: CONST.ICON_TYPE_AVATAR, name: secondaryDisplayName ?? '', id: secondaryAccountId, @@ -129,12 +126,11 @@ function ReportActionItemSingle({ } else { secondaryAvatar = {name: '', source: '', type: 'avatar'}; } - const icon = { - source: avatarSource ?? FallbackAvatar, + source: avatarSource, type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, name: primaryDisplayName ?? '', - id: avatarAccountId, + id: isWorkspaceActor ? '' : actorAccountID, }; // Since the display name for a report action message is delivered with the report history as an array of fragments @@ -205,7 +201,6 @@ function ReportActionItemSingle({ type={icon.type} name={icon.name} fallbackIcon={fallbackIcon} - accountID={icon.id} /> </View> </UserDetailsTooltip> diff --git a/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx b/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx index b0287efb8990..e7726fb89537 100644 --- a/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx +++ b/src/pages/home/sidebar/ProfileAvatarWithIndicator.tsx @@ -5,6 +5,7 @@ import AvatarWithIndicator from '@components/AvatarWithIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as UserUtils from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; type ProfileAvatarWithIndicatorProps = { @@ -22,8 +23,7 @@ function ProfileAvatarWithIndicator({isSelected = false}: ProfileAvatarWithIndic <OfflineWithFeedback pendingAction={currentUserPersonalDetails.pendingFields?.avatar}> <View style={[isSelected && styles.selectedAvatarBorder]}> <AvatarWithIndicator - source={currentUserPersonalDetails.avatar} - accountID={currentUserPersonalDetails.accountID} + source={UserUtils.getAvatar(currentUserPersonalDetails.avatar, currentUserPersonalDetails.accountID)} fallbackIcon={currentUserPersonalDetails.fallbackIcon} isLoading={isLoading && !currentUserPersonalDetails.avatar} /> diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index b18522662dde..30d92363afcd 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -14,7 +14,6 @@ import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/Bu import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import MessagesRow from '@components/MessagesRow'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -37,6 +36,7 @@ import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/typ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as UserUtils from '@libs/UserUtils'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -353,7 +353,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, rightElement: roleBadge, icons: [ { - source: details.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(details.avatar, accountID), name: formatPhoneNumber(details?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: accountID, diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 17d5f56d1dbf..32b43a230619 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -18,6 +18,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as UserUtils from '@libs/UserUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; @@ -57,6 +58,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const memberLogin = personalDetails?.[accountID]?.login ?? ''; const member = policy?.employeeList?.[memberLogin]; const details = personalDetails?.[accountID] ?? ({} as PersonalDetails); + const avatar = details.avatar ?? UserUtils.getDefaultAvatar(); const fallbackIcon = details.fallbackIcon ?? ''; const displayName = details.displayName ?? ''; const isSelectedMemberOwner = policy?.owner === details.login; @@ -140,10 +142,9 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM <Avatar containerStyles={[styles.avatarXLarge, styles.mv5, styles.noOutline]} imageStyles={[styles.avatarXLarge]} - source={details.avatar} + source={UserUtils.getAvatar(avatar, accountID)} size={CONST.AVATAR_SIZE.XLARGE} fallbackIcon={fallbackIcon} - accountID={accountID} /> </OfflineWithFeedback> {Boolean(details.displayName ?? '') && ( diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx index 8be9afd790d8..74434fbdd1f8 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx @@ -5,7 +5,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import Badge from '@components/Badge'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {ListItem, Section} from '@components/SelectionList/types'; @@ -19,6 +18,7 @@ import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/typ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as UserUtils from '@libs/UserUtils'; import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -87,7 +87,7 @@ function WorkspaceWorkflowsApproverPage({policy, personalDetails, isLoadingRepor rightElement: roleBadge, icons: [ { - source: details.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(details.avatar, accountID), name: formatPhoneNumber(details?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: accountID, diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx index 6a6d64d51eb1..3bd4ab9003c5 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -5,7 +5,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import Badge from '@components/Badge'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import type {ListItem, Section} from '@components/SelectionList/types'; @@ -18,6 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as UserUtils from '@libs/UserUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; @@ -88,7 +88,7 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR rightElement: roleBadge, icons: [ { - source: details?.avatar ?? FallbackAvatar, + source: UserUtils.getAvatar(details?.avatar, accountID), name: formatPhoneNumber(details?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: accountID, diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index 9ac596a85777..8b96a89a2a1b 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -34,7 +34,7 @@ type Icon = { name?: string; /** Avatar id */ - id?: number; + id?: number | string; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ fallbackIcon?: AvatarSource; From f11aa539c5f8d9f589c9e51e46d3d6d82c37d2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= <michal.skalka@swmansion.com> Date: Fri, 26 Apr 2024 14:02:38 +0200 Subject: [PATCH 467/580] Fix cursor positioning on focus --- src/components/Composer/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 43a100cf3c90..4bc54d13b056 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -118,11 +118,8 @@ function Composer( * Adds the cursor position to the selection change event. */ const addCursorPositionToSelectionChange = (event: NativeSyntheticEvent<TextInputSelectionChangeEventData>) => { - if (!isRendered) { - return; - } const webEvent = event as BaseSyntheticEvent<TextInputSelectionChangeEventData>; - if (shouldCalculateCaretPosition) { + if (shouldCalculateCaretPosition && isRendered) { // we do flushSync to make sure that the valueBeforeCaret is updated before we calculate the caret position to receive a proper position otherwise we will calculate position for the previous state flushSync(() => { setValueBeforeCaret(webEvent.target.value.slice(0, webEvent.nativeEvent.selection.start)); From 13273b626c35105006d9c4c3d2cd26451111417f Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Fri, 26 Apr 2024 14:11:09 +0200 Subject: [PATCH 468/580] Rename ExpenseListItem to TransactionListItem --- .../SelectionList/TemporaryExpenseListItem.tsx | 4 ++-- .../{ExpenseListItem.tsx => TransactionListItem.tsx} | 10 +++++----- src/components/SelectionList/types.ts | 8 ++++---- src/libs/SearchUtils.ts | 7 ++++--- src/pages/Search/SearchPage.tsx | 4 ++-- 5 files changed, 17 insertions(+), 16 deletions(-) rename src/components/SelectionList/{ExpenseListItem.tsx => TransactionListItem.tsx} (97%) diff --git a/src/components/SelectionList/TemporaryExpenseListItem.tsx b/src/components/SelectionList/TemporaryExpenseListItem.tsx index 011a6d73ac4f..49544c4f5557 100644 --- a/src/components/SelectionList/TemporaryExpenseListItem.tsx +++ b/src/components/SelectionList/TemporaryExpenseListItem.tsx @@ -6,7 +6,7 @@ import type {SearchTransaction} from '@src/types/onyx/SearchResults'; // NOTE: This is a completely temporary mock item so that something can be displayed in SearchWidget // This should be removed and implement properly in: https://github.com/Expensify/App/issues/39877 -function ExpenseListItem({item}: {item: SearchTransaction}) { +function TransactionListItem({item}: {item: SearchTransaction}) { const styles = useThemeStyles(); return ( <View style={[styles.pt8]}> @@ -15,4 +15,4 @@ function ExpenseListItem({item}: {item: SearchTransaction}) { ); } -export default ExpenseListItem; +export default TransactionListItem; diff --git a/src/components/SelectionList/ExpenseListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx similarity index 97% rename from src/components/SelectionList/ExpenseListItem.tsx rename to src/components/SelectionList/TransactionListItem.tsx index a460bd3b3d01..00d80c6ce11b 100644 --- a/src/components/SelectionList/ExpenseListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -11,9 +11,9 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import type {ExpenseListItemProps, ListItem} from './types'; +import type {TransactionListItemProps, ListItem} from './types'; -function ExpenseListItem<TItem extends ListItem>({ +function TransactionListItem<TItem extends ListItem>({ item, isFocused, showTooltip, @@ -26,7 +26,7 @@ function ExpenseListItem<TItem extends ListItem>({ rightHandSideComponent, onFocus, shouldSyncFocus, -}: ExpenseListItemProps<TItem>) { +}: TransactionListItemProps<TItem>) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); @@ -213,6 +213,6 @@ function ExpenseListItem<TItem extends ListItem>({ ); } -ExpenseListItem.displayName = 'ExpenseListItem'; +TransactionListItem.displayName = 'TransactionListItem'; -export default ExpenseListItem; +export default TransactionListItem; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index f7c3697b2814..b72bba02ee19 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -7,7 +7,7 @@ import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type IconAsset from '@src/types/utils/IconAsset'; -import type ExpenseListItem from './ExpenseListItem'; +import type TransactionListItem from './TransactionListItem'; import type InviteMemberListItem from './InviteMemberListItem'; import type RadioListItem from './RadioListItem'; import type TableListItem from './TableListItem'; @@ -181,9 +181,9 @@ type RadioListItemProps<TItem extends ListItem> = ListItemProps<TItem>; type TableListItemProps<TItem extends ListItem> = ListItemProps<TItem>; -type ExpenseListItemProps<TItem extends ListItem> = ListItemProps<TItem>; +type TransactionListItemProps<TItem extends ListItem> = ListItemProps<TItem>; -type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem | typeof ExpenseListItem; +type ValidListItem = typeof RadioListItem | typeof UserListItem | typeof TableListItem | typeof InviteMemberListItem | typeof TransactionListItem; type Section<TItem extends ListItem> = { /** Title of the section */ @@ -376,7 +376,7 @@ export type { RadioListItemProps, TableListItemProps, InviteMemberListItemProps, - ExpenseListItemProps, + TransactionListItemProps, ListItem, ListItemProps, FlattenedSectionsReturn, diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index ccced12a0bc4..c78a7a48c297 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,10 +1,11 @@ -import ExpenseListItem from '@components/SelectionList/TemporaryExpenseListItem'; + +import TransactionListItem from '@components/SelectionList/TransactionListItem'; import type * as OnyxTypes from '@src/types/onyx'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; const searchTypeToItemMap = { transaction: { - listItem: ExpenseListItem, + listItem: TransactionListItem, }, }; @@ -17,7 +18,7 @@ const getTransactionsSections = (data: OnyxTypes.SearchResults['data']): SearchT * TODO: in future make this function generic and return specific item component based on type * For now only 1 search item type exists in the app so this function is simplified */ -function getListItem(): typeof ExpenseListItem { +function getListItem(): typeof TransactionListItem { return searchTypeToItemMap.transaction.listItem; } diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 2ddea460de10..22482e33ab4e 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -6,7 +6,7 @@ import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; import SelectionList from '@components/SelectionList'; -import ExpenseListItem from '@components/SelectionList/ExpenseListItem'; +import TransactionListItem from '@components/SelectionList/TransactionListItem'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -101,7 +101,7 @@ function SearchPage({route}: SearchPageProps) { <SelectionList canSelectMultiple customListHeader={getListHeader()} - ListItem={ExpenseListItem} + ListItem={TransactionListItem} onSelectRow={() => {}} onSelectAll={() => {}} sections={[{data, isDisabled: false}]} From 04587529c16de09d1105788bb92d20acf97d4c23 Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Fri, 26 Apr 2024 14:16:29 +0200 Subject: [PATCH 469/580] fix pr comments --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index fb98fe23192d..239286db0b1d 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -111,7 +111,7 @@ function BaseGetPhysicalCard({ const domainCards = CardUtils.getDomainCards(cardList)[domain] || []; const cardToBeIssued = domainCards.find((card) => !card?.nameValuePairs?.isVirtual && card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_NOT_ISSUED); - const cardID = cardToBeIssued?.cardID ?? 0; + const cardID = cardToBeIssued?.cardID.toString() ?? ''; useEffect(() => { if (isRouteSet.current || !privatePersonalDetails || !cardList) { @@ -148,7 +148,7 @@ function BaseGetPhysicalCard({ const updatedPrivatePersonalDetails = GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(draftValues); // If the current step of the get physical card flow is the confirmation page if (isConfirmation) { - Wallet.requestPhysicalExpensifyCard(cardID, session?.authToken ?? '', updatedPrivatePersonalDetails); + Wallet.requestPhysicalExpensifyCard(cardToBeIssued?.cardID ?? 0, session?.authToken ?? '', updatedPrivatePersonalDetails); // Form draft data needs to be erased when the flow is complete, // so that no stale data is left on Onyx FormActions.clearDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM); @@ -165,7 +165,7 @@ function BaseGetPhysicalCard({ > <HeaderWithBackButton title={title} - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID.toString()))} + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(cardID))} /> <Text style={[styles.textHeadline, styles.mh5, styles.mb5]}>{headline}</Text> {renderContent({onSubmit, submitButtonText, children, onValidate})} From fcbc5acce04c138fde3245641a8b7ce85f6f2555 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Fri, 26 Apr 2024 12:17:33 +0000 Subject: [PATCH 470/580] Update version to 1.4.66-4 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7d3048bc8eb8..aff57eba7a39 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046603 - versionName "1.4.66-3" + versionCode 1001046604 + versionName "1.4.66-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 6208f4645762..52d3d861bde3 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.66.3</string> + <string>1.4.66.4</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 28efc0914959..3d24470c22ec 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.66.3</string> + <string>1.4.66.4</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 1ef215d43912..22c83ac6e5f2 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.66</string> <key>CFBundleVersion</key> - <string>1.4.66.3</string> + <string>1.4.66.4</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index 989a629caf0a..3d1a9426f9aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.66-3", + "version": "1.4.66-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.66-3", + "version": "1.4.66-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 751108d025fd..1e0dee0c0967 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.66-3", + "version": "1.4.66-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From f9159056bf19f5f23a98f6db06670be98db5724e Mon Sep 17 00:00:00 2001 From: Jakub Szymczak <szymczak.jakub12@gmail.com> Date: Fri, 26 Apr 2024 14:26:05 +0200 Subject: [PATCH 471/580] fix lint errors --- src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx index 239286db0b1d..8fff374e0c8e 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.tsx @@ -156,7 +156,7 @@ function BaseGetPhysicalCard({ return; } GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, updatedPrivatePersonalDetails); - }, [cardID, domain, draftValues, isConfirmation, session?.authToken]); + }, [cardID, cardToBeIssued?.cardID, domain, draftValues, isConfirmation, session?.authToken]); return ( <ScreenWrapper shouldEnablePickerAvoiding={false} From c6f599adc86585b505a355b00916d0826876f0c5 Mon Sep 17 00:00:00 2001 From: Vit Horacek <36083550+mountiny@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:00:02 +0200 Subject: [PATCH 472/580] Revert "Fix unread issue in combine report" --- .../LHNOptionsList/LHNOptionsList.tsx | 3 --- .../LHNOptionsList/OptionRowLHN.tsx | 5 ---- .../LHNOptionsList/OptionRowLHNData.tsx | 3 --- src/components/LHNOptionsList/types.ts | 3 --- src/libs/ReportUtils.ts | 1 - src/libs/SidebarUtils.ts | 5 +--- .../BaseReportActionContextMenu.tsx | 5 ---- .../report/ContextMenu/ContextMenuActions.tsx | 6 +---- .../PopoverReportActionContextMenu.tsx | 4 --- .../ContextMenu/ReportActionContextMenu.ts | 3 --- src/pages/home/report/ReportActionItem.tsx | 1 - src/pages/home/report/ReportActionsList.tsx | 26 +++---------------- tests/perf-test/SidebarUtils.perf-test.ts | 1 - 13 files changed, 5 insertions(+), 61 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 469f17258a7f..8c43ae542932 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -116,8 +116,6 @@ function LHNOptionsList({ const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); const lastReportAction = sortedReportActions[0]; - const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, itemReportActions); - const transactionThreadReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; // Get the transaction for the last report action let lastReportActionTransactionID = ''; @@ -131,7 +129,6 @@ function LHNOptionsList({ <OptionRowLHNData reportID={reportID} fullReport={itemFullReport} - transactionThreadReport={transactionThreadReport} reportActions={itemReportActions} parentReportAction={itemParentReportAction} policy={itemPolicy} diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 2eba8b62dfd7..9946dea9d5a7 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -96,11 +96,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti false, optionItem.isPinned, !!optionItem.isUnread, - [], - false, - () => {}, - false, - optionItem.transactionThreadReportID, ); }; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 1d6cc636f939..c80017c39a3d 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -27,7 +27,6 @@ function OptionRowLHNData({ lastReportActionTransaction = {}, transactionViolations, canUseViolations, - transactionThreadReport, ...propsToForward }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; @@ -48,7 +47,6 @@ function OptionRowLHNData({ policy, parentReportAction, hasViolations: !!shouldDisplayViolations, - transactionThreadReport, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; @@ -72,7 +70,6 @@ function OptionRowLHNData({ transactionViolations, canUseViolations, receiptTransactions, - transactionThreadReport, ]); return ( diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 79b5f5ad3889..0f0c921747b4 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -74,9 +74,6 @@ type OptionRowLHNDataProps = { /** The full data of the report */ fullReport: OnyxEntry<Report>; - /** The transaction thread report associated with the current report – applicable only for one-transaction money reports */ - transactionThreadReport: OnyxEntry<Report>; - /** The policy which the user has access to and which the report could be tied to */ policy?: OnyxEntry<Policy>; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ca5080cc4afc..e406b750bfd3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -441,7 +441,6 @@ type OptionData = { reportID?: string; enabled?: boolean; data?: Partial<TaxRate>; - transactionThreadReportID?: string | null; } & Report; type OnyxDataTaskAssigneeChat = { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 248878df2bf8..4f1a35ee1d87 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -190,7 +190,6 @@ function getOptionData({ policy, parentReportAction, hasViolations, - transactionThreadReport, }: { report: OnyxEntry<Report>; reportActions: OnyxEntry<ReportActions>; @@ -199,7 +198,6 @@ function getOptionData({ policy: OnyxEntry<Policy> | undefined; parentReportAction: OnyxEntry<ReportAction> | undefined; hasViolations: boolean; - transactionThreadReport: OnyxEntry<Report>; }): ReportUtils.OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do @@ -236,7 +234,6 @@ function getOptionData({ isWaitingOnBankAccount: false, isAllowedToComment: true, isDeletedParentAction: false, - transactionThreadReportID: transactionThreadReport?.reportID, }; let participantAccountIDs = report.participantAccountIDs ?? []; @@ -269,7 +266,7 @@ function getOptionData({ result.statusNum = report.statusNum; // When the only message of a report is deleted lastVisibileActionCreated is not reset leading to wrongly // setting it Unread so we add additional condition here to avoid empty chat LHN from being bold. - result.isUnread = (ReportUtils.isUnread(report) && !!report.lastActorAccountID) || (ReportUtils.isUnread(transactionThreadReport) && !!transactionThreadReport?.lastActorAccountID); + result.isUnread = ReportUtils.isUnread(report) && !!report.lastActorAccountID; result.isUnreadWithMention = ReportUtils.isUnreadWithMention(report); result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 8e2c9d6f09b3..46ebdd751762 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -49,9 +49,6 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types originalReportID: string; - /** The ID of transaction thread report associated with the current report, if any */ - transactionThreadReportID: string; - /** * If true, this component will be a small, row-oriented menu that displays icons but not text. * If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row. @@ -120,7 +117,6 @@ function BaseReportActionContextMenu({ checkIfContextMenuActive, disabledActions = [], setIsEmojiPickerActive, - transactionThreadReportID, }: BaseReportActionContextMenuProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -249,7 +245,6 @@ function BaseReportActionContextMenu({ interceptAnonymousUser, openOverflowMenu, setIsEmojiPickerActive, - transactionThreadReportID, }; if ('renderContent' in contextAction) { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index a09f0355b996..438d09e778b4 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -79,7 +79,6 @@ type ContextMenuActionPayload = { event?: GestureResponderEvent | MouseEvent | KeyboardEvent; setIsEmojiPickerActive?: (state: boolean) => void; anchorRef?: MutableRefObject<View | null>; - transactionThreadReportID?: string; }; type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; @@ -214,11 +213,8 @@ const ContextMenuActions: ContextMenuAction[] = [ successIcon: Expensicons.Checkmark, shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, - onPress: (closePopover, {reportID, transactionThreadReportID}) => { + onPress: (closePopover, {reportID}) => { Report.readNewestAction(reportID); - if (transactionThreadReportID && transactionThreadReportID !== '0') { - Report.readNewestAction(transactionThreadReportID); - } if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 38fd37ee4d26..6cb688ff2558 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -37,7 +37,6 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef<Repor const reportActionRef = useRef<OnyxEntry<ReportAction>>(null); const reportActionIDRef = useRef('0'); const originalReportIDRef = useRef('0'); - const transactionThreadReportIDRef = useRef('0'); const selectionRef = useRef(''); const reportActionDraftMessageRef = useRef<string>(); @@ -172,7 +171,6 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef<Repor shouldCloseOnTarget = false, setIsEmojiPickerActive = () => {}, isOverflowMenu = false, - transactionThreadReportID = undefined, ) => { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -214,7 +212,6 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef<Repor reportIDRef.current = reportID ?? '0'; reportActionIDRef.current = reportActionID ?? '0'; originalReportIDRef.current = originalReportID ?? '0'; - transactionThreadReportIDRef.current = transactionThreadReportID ?? '0'; selectionRef.current = selection; setIsPopoverVisible(true); reportActionDraftMessageRef.current = draftMessage; @@ -340,7 +337,6 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef<Repor type={typeRef.current} reportID={reportIDRef.current} reportActionID={reportActionIDRef.current} - transactionThreadReportID={transactionThreadReportIDRef.current} draftMessage={reportActionDraftMessageRef.current} selection={selectionRef.current} isArchivedRoom={isRoomArchived} diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 0d2c6a857909..f984e88b85b2 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -37,7 +37,6 @@ type ShowContextMenu = ( shouldCloseOnTarget?: boolean, setIsEmojiPickerActive?: (state: boolean) => void, isOverflowMenu?: boolean, - transactionThreadReportID?: string, ) => void; type ReportActionContextMenu = { @@ -120,7 +119,6 @@ function showContextMenu( shouldCloseOnTarget = false, setIsEmojiPickerActive = () => {}, isOverflowMenu = false, - transactionThreadReportID = '0', ) { if (!contextMenuRef.current) { return; @@ -151,7 +149,6 @@ function showContextMenu( shouldCloseOnTarget, setIsEmojiPickerActive, isOverflowMenu, - transactionThreadReportID, ); } diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index ecdffc9ac627..c07b693001e0 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -919,7 +919,6 @@ function ReportActionItem({ isChronosReport={ReportUtils.chatIncludesChronos(originalReport)} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} setIsEmojiPickerActive={setIsEmojiPickerActive} - transactionThreadReportID={transactionThreadReport?.reportID ?? '0'} /> <View style={StyleUtils.getReportActionItemStyle(hovered || isWhisper || isContextMenuActive || !!isEmojiPickerActive || draftMessage !== undefined, !!onPress)}> <OfflineWithFeedback diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 6f59c1b162c9..3c6038697c67 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -4,7 +4,7 @@ import type {RouteProp} from '@react-navigation/native'; import type {DebouncedFunc} from 'lodash'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; -import type {EmitterSubscription, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -200,8 +200,7 @@ function ReportActionsList({ ); const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; const reportActionSize = useRef(sortedVisibleReportActions.length); - const hasNewestReportAction = - sortedReportActions?.[0].created === report.lastVisibleActionCreated || sortedReportActions?.[0].created === transactionThreadReport?.lastVisibleActionCreated; + const hasNewestReportAction = sortedReportActions?.[0].created === report.lastVisibleActionCreated; const hasNewestReportActionRef = useRef(hasNewestReportAction); hasNewestReportActionRef.current = hasNewestReportAction; const previousLastIndex = useRef(lastActionIndex); @@ -307,28 +306,12 @@ function ReportActionsList({ setMessageManuallyMarkedUnread(new Date().getTime()); }); - let unreadActionSubscriptionForTransactionThread: EmitterSubscription | undefined; - let readNewestActionSubscriptionForTransactionThread: EmitterSubscription | undefined; - if (transactionThreadReport?.reportID) { - unreadActionSubscriptionForTransactionThread = DeviceEventEmitter.addListener(`unreadAction_${transactionThreadReport?.reportID}`, (newLastReadTime) => { - resetUnreadMarker(newLastReadTime); - setMessageManuallyMarkedUnread(new Date().getTime()); - }); - - readNewestActionSubscriptionForTransactionThread = DeviceEventEmitter.addListener(`readNewestAction_${transactionThreadReport?.reportID}`, (newLastReadTime) => { - resetUnreadMarker(newLastReadTime); - setMessageManuallyMarkedUnread(0); - }); - } - return () => { unreadActionSubscription.remove(); readNewestActionSubscription.remove(); deletedReportActionSubscription.remove(); - unreadActionSubscriptionForTransactionThread?.remove(); - readNewestActionSubscriptionForTransactionThread?.remove(); }; - }, [report.reportID, transactionThreadReport?.reportID]); + }, [report.reportID]); useEffect(() => { if (linkedReportActionID) { @@ -416,9 +399,6 @@ function ReportActionsList({ reportScrollManager.scrollToBottom(); readActionSkipped.current = false; Report.readNewestAction(report.reportID); - if (transactionThreadReport?.reportID) { - Report.readNewestAction(transactionThreadReport?.reportID); - } }; /** diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 75503e5179a4..cceb3ae437b9 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -107,7 +107,6 @@ describe('SidebarUtils', () => { policy, parentReportAction, hasViolations: false, - transactionThreadReport: null, }), ); }); From 2487ab7b7cc8f1a2ae4c0dda84d5f649cdbbddc6 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Fri, 26 Apr 2024 14:16:30 +0000 Subject: [PATCH 473/580] Update version to 1.4.66-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index aff57eba7a39..2d690ed72b8c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046604 - versionName "1.4.66-4" + versionCode 1001046605 + versionName "1.4.66-5" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 52d3d861bde3..42b54741d190 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.66.4</string> + <string>1.4.66.5</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3d24470c22ec..f1986986a1a8 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.66.4</string> + <string>1.4.66.5</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 22c83ac6e5f2..de4dba072fbb 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.66</string> <key>CFBundleVersion</key> - <string>1.4.66.4</string> + <string>1.4.66.5</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index 3d1a9426f9aa..5d91baef8a44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.66-4", + "version": "1.4.66-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.66-4", + "version": "1.4.66-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1e0dee0c0967..fdb6155e24b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.66-4", + "version": "1.4.66-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From ace1043b94aed0714ae1946c6903b7b0993f8d19 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Fri, 26 Apr 2024 20:25:25 +0530 Subject: [PATCH 474/580] fix lint warning. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index e0d324db845e..d20b7be63092 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -384,6 +384,7 @@ function MoneyRequestView({ } /> )} + {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {!(!hasReceipt && (canEditReceipt || isAdmin || isApprover)) && !(showMapAsImage || hasReceipt) && <View style={{marginVertical: 6}} />} {shouldShowNotesViolations && <ReceiptAuditMessages notes={noteTypeViolations} />} <ViolationMessages violations={getViolationsForField('receipt')} /> From 9076bc02fae5f100110ebdd80d5ce4a34e875be1 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 17:01:23 +0200 Subject: [PATCH 475/580] rename the component --- src/pages/home/report/ReportFooter.tsx | 4 ++-- ...Message.tsx => SystemChatReportFooterMessage.tsx} | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) rename src/pages/home/report/{OnboardingReportFooterMessage.tsx => SystemChatReportFooterMessage.tsx} (88%) diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 73ab8fcbac40..11d9a0a4871d 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -20,8 +20,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; -import OnboardingReportFooterMessage from './OnboardingReportFooterMessage'; import ReportActionCompose from './ReportActionCompose/ReportActionCompose'; +import SystemChatReportFooterMessage from './SystemChatReportFooterMessage'; type ReportFooterOnyxProps = { /** Whether to show the compose input */ @@ -141,7 +141,7 @@ function ReportFooter({ /> )} {isArchivedRoom && <ArchivedReportFooter report={report} />} - {!canWriteInReport && <OnboardingReportFooterMessage />} + {!canWriteInReport && <SystemChatReportFooterMessage />} {!isSmallScreenWidth && <View style={styles.offlineIndicatorRow}>{hideComposer && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}</View>} </View> )} diff --git a/src/pages/home/report/OnboardingReportFooterMessage.tsx b/src/pages/home/report/SystemChatReportFooterMessage.tsx similarity index 88% rename from src/pages/home/report/OnboardingReportFooterMessage.tsx rename to src/pages/home/report/SystemChatReportFooterMessage.tsx index 41f352c6c032..8316189215c1 100644 --- a/src/pages/home/report/OnboardingReportFooterMessage.tsx +++ b/src/pages/home/report/SystemChatReportFooterMessage.tsx @@ -16,7 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy as PolicyType, Report} from '@src/types/onyx'; -type OnboardingReportFooterMessageOnyxProps = { +type SystemChatReportFooterMessageOnyxProps = { /** Saved onboarding purpose selected by the user */ choice: OnyxEntry<OnboardingPurposeType>; @@ -27,9 +27,9 @@ type OnboardingReportFooterMessageOnyxProps = { policies: OnyxCollection<PolicyType>; }; -type OnboardingReportFooterMessageProps = OnboardingReportFooterMessageOnyxProps; +type SystemChatReportFooterMessageProps = SystemChatReportFooterMessageOnyxProps; -function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingReportFooterMessageProps) { +function SystemChatReportFooterMessage({choice, reports, policies}: SystemChatReportFooterMessageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -73,9 +73,9 @@ function OnboardingReportFooterMessage({choice, reports, policies}: OnboardingRe ); } -OnboardingReportFooterMessage.displayName = 'OnboardingReportFooterMessage'; +SystemChatReportFooterMessage.displayName = 'SystemChatReportFooterMessage'; -export default withOnyx<OnboardingReportFooterMessageProps, OnboardingReportFooterMessageOnyxProps>({ +export default withOnyx<SystemChatReportFooterMessageProps, SystemChatReportFooterMessageOnyxProps>({ choice: { key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, }, @@ -85,4 +85,4 @@ export default withOnyx<OnboardingReportFooterMessageProps, OnboardingReportFoot policies: { key: ONYXKEYS.COLLECTION.POLICY, }, -})(OnboardingReportFooterMessage); +})(SystemChatReportFooterMessage); From 62b193a12ef1e23b8a31c8fe604100d622777565 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 17:02:10 +0200 Subject: [PATCH 476/580] rename keys --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/pages/home/report/SystemChatReportFooterMessage.tsx | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b1cd28063032..1b935b67c616 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2763,7 +2763,7 @@ export default { }, copyReferralLink: 'Copy invite link', }, - onboardingBottomMessage: { + systemChatFooterMessage: { [CONST.INTRO_CHOICES.MANAGE_TEAM]: { phrase1: 'Chat with your setup specialist in ', phrase2: ' for help', diff --git a/src/languages/es.ts b/src/languages/es.ts index 758cae8d5416..2677b2573a0a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3256,7 +3256,7 @@ export default { }, copyReferralLink: 'Copiar enlace de invitación', }, - onboardingBottomMessage: { + systemChatFooterMessage: { [CONST.INTRO_CHOICES.MANAGE_TEAM]: { phrase1: 'Chatea con tu especialista asignado en ', phrase2: ' para obtener ayuda', diff --git a/src/pages/home/report/SystemChatReportFooterMessage.tsx b/src/pages/home/report/SystemChatReportFooterMessage.tsx index 8316189215c1..6e5688282b47 100644 --- a/src/pages/home/report/SystemChatReportFooterMessage.tsx +++ b/src/pages/home/report/SystemChatReportFooterMessage.tsx @@ -45,19 +45,19 @@ function SystemChatReportFooterMessage({choice, reports, policies}: SystemChatRe case CONST.ONBOARDING_CHOICES.MANAGE_TEAM: return ( <> - {translate('onboardingBottomMessage.newDotManageTeam.phrase1')} + {translate('systemChatFooterMessage.newDotManageTeam.phrase1')} <TextLink onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminChatReport?.reportID ?? ''))}> {adminChatReport?.reportName ?? CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS} </TextLink> - {translate('onboardingBottomMessage.newDotManageTeam.phrase2')} + {translate('systemChatFooterMessage.newDotManageTeam.phrase2')} </> ); default: return ( <> - {translate('onboardingBottomMessage.default.phrase1')} + {translate('systemChatFooterMessage.default.phrase1')} <TextLink onPress={() => ReportInstance.navigateToConciergeChat()}>{CONST?.CONCIERGE_CHAT_NAME}</TextLink> - {translate('onboardingBottomMessage.default.phrase2')} + {translate('systemChatFooterMessage.default.phrase2')} </> ); } From e448d96b029c3228df18db95c74b4dd321692e99 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 26 Apr 2024 17:07:22 +0200 Subject: [PATCH 477/580] Update Send From list display --- src/pages/iou/request/step/IOURequestStepSendFrom.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index cdc7e9073c5a..b61f3d337826 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -37,6 +37,8 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte const {translate} = useLocalize(); const {transactionID, backTo} = route.params; + const selectedWorkspace = useMemo(() => transaction?.participants?.find((participant) => participant.isSender), [transaction]); + const workspaceOptions: WorkspaceListItem[] = useMemo(() => { const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies); return activeAdminWorkspaces.map((policy) => ({ @@ -51,7 +53,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte type: CONST.ICON_TYPE_WORKSPACE, }, ], - isSelected: !!transaction?.participants?.find((participant) => participant.policyID === policy.id), + isSelected: selectedWorkspace?.policyID === policy.id, })); }, [allPolicies, transaction]); @@ -84,6 +86,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte sections={[{data: workspaceOptions, title: translate('common.workspaces')}]} onSelectRow={selectWorkspace} ListItem={UserListItem} + initiallyFocusedOptionKey={selectedWorkspace?.policyID} /> </StepScreenWrapper> ); From 90a72e9fadd1df42c4cee89759647da137d28915 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 17:11:03 +0200 Subject: [PATCH 478/580] simplify admin chat getting --- .../report/SystemChatReportFooterMessage.tsx | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/pages/home/report/SystemChatReportFooterMessage.tsx b/src/pages/home/report/SystemChatReportFooterMessage.tsx index 6e5688282b47..a946968f0ec7 100644 --- a/src/pages/home/report/SystemChatReportFooterMessage.tsx +++ b/src/pages/home/report/SystemChatReportFooterMessage.tsx @@ -1,6 +1,6 @@ import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import Banner from '@components/Banner'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; @@ -8,37 +8,32 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; import * as ReportInstance from '@userActions/Report'; import type {OnboardingPurposeType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy as PolicyType, Report} from '@src/types/onyx'; type SystemChatReportFooterMessageOnyxProps = { /** Saved onboarding purpose selected by the user */ choice: OnyxEntry<OnboardingPurposeType>; - /** Collection of reports */ - reports: OnyxCollection<Report>; - - /** The list of this user's policies */ - policies: OnyxCollection<PolicyType>; + /** policyID for main workspace */ + activePolicyID: OnyxEntry<Required<string>>; }; type SystemChatReportFooterMessageProps = SystemChatReportFooterMessageOnyxProps; -function SystemChatReportFooterMessage({choice, reports, policies}: SystemChatReportFooterMessageProps) { +function SystemChatReportFooterMessage({choice, activePolicyID}: SystemChatReportFooterMessageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const adminChatReport = useMemo(() => { - const adminsReports = Object.values(reports ?? {}).filter((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS); - const activePolicies = Object.values(policies ?? {}).filter((policy) => PolicyUtils.shouldShowPolicy(policy, false)); - - return adminsReports.find((report) => activePolicies.find((policy) => policy?.id === report?.policyID)); - }, [policies, reports]); + const policy = PolicyUtils.getPolicy(activePolicyID ?? ''); + return ReportUtils.getReport(String(policy.chatReportIDAdmins)); + }, [activePolicyID]); const content = useMemo(() => { switch (choice) { @@ -79,10 +74,9 @@ export default withOnyx<SystemChatReportFooterMessageProps, SystemChatReportFoot choice: { key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, + + activePolicyID: { + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + initialValue: null, }, })(SystemChatReportFooterMessage); From 0ec7b8a4ddea409a375c01d19dbbb02e1de80f09 Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Fri, 26 Apr 2024 17:16:55 +0200 Subject: [PATCH 479/580] Modify TransactionListItem --- .../TemporaryExpenseListItem.tsx | 18 -- .../SelectionList/TransactionListItem.tsx | 203 ++++++++---------- src/pages/Search/SearchPage.tsx | 28 +-- src/styles/index.ts | 5 + 4 files changed, 110 insertions(+), 144 deletions(-) delete mode 100644 src/components/SelectionList/TemporaryExpenseListItem.tsx diff --git a/src/components/SelectionList/TemporaryExpenseListItem.tsx b/src/components/SelectionList/TemporaryExpenseListItem.tsx deleted file mode 100644 index 49544c4f5557..000000000000 --- a/src/components/SelectionList/TemporaryExpenseListItem.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import useThemeStyles from '@hooks/useThemeStyles'; -import type {SearchTransaction} from '@src/types/onyx/SearchResults'; - -// NOTE: This is a completely temporary mock item so that something can be displayed in SearchWidget -// This should be removed and implement properly in: https://github.com/Expensify/App/issues/39877 -function TransactionListItem({item}: {item: SearchTransaction}) { - const styles = useThemeStyles(); - return ( - <View style={[styles.pt8]}> - <Text>Item: {item.transactionID}</Text> - </View> - ); -} - -export default TransactionListItem; diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 00d80c6ce11b..9bd2e21bc011 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -1,17 +1,22 @@ import {format} from 'date-fns'; import React, {useCallback} from 'react'; import {View} from 'react-native'; +import Avatar from '@components/Avatar'; +import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MultipleAvatars from '@components/MultipleAvatars'; +import {usePersonalDetails} from '@components/OnyxProvider'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; import TextWithTooltip from '@components/TextWithTooltip'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import type {TransactionListItemProps, ListItem} from './types'; +import type {ListItem, TransactionListItemProps} from './types'; function TransactionListItem<TItem extends ListItem>({ item, @@ -30,6 +35,7 @@ function TransactionListItem<TItem extends ListItem>({ const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); + const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; @@ -41,6 +47,8 @@ function TransactionListItem<TItem extends ListItem>({ } }, [item, onCheckboxPress, onSelectRow]); + console.log('personalDetails', personalDetails); + return ( <BaseListItem item={item} @@ -96,116 +104,93 @@ function TransactionListItem<TItem extends ListItem>({ ]} /> )} - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={format(new Date(item.created), 'MMM dd')} - style={[ - styles.optionDisplayName, - isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, - styles.sidebarLinkText, - styles.pre, - styles.justifyContentCenter, - ]} - /> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.description} - style={[ - styles.optionDisplayName, - isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, - styles.sidebarLinkText, - styles.pre, - styles.justifyContentCenter, - ]} - /> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.from.displayName} - style={[ - styles.optionDisplayName, - isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, - styles.sidebarLinkText, - styles.pre, - - styles.justifyContentCenter, - ]} - /> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.to.displayName} - style={[ - styles.optionDisplayName, - isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, - styles.sidebarLinkText, - styles.pre, - - styles.justifyContentCenter, - ]} - /> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.category} - style={[ - styles.optionDisplayName, - styles.sidebarLinkActiveText, - styles.pre, - styles.justifyContentCenter, - ]} - /> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.tag} - style={[ - styles.optionDisplayName, - isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, - styles.sidebarLinkText, - styles.pre, - - styles.justifyContentCenter, - ]} - /> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsEnd]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.amount} - style={[ - styles.optionDisplayName, - isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, - styles.sidebarLinkText, - styles.pre, + <View style={[styles.flexRow, styles.flex1, styles.gap3]}> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={format(new Date(item.created), 'MMM dd')} + style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.description} + style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <View style={[styles.flexRow, styles.gap3, styles.flex1, styles.alignItemsCenter]}> + <Avatar + imageStyles={[styles.alignSelfCenter]} + size={CONST.AVATAR_SIZE.SMALL} + source={personalDetails[item.managerID]?.avatar} + name={personalDetails[item.managerID]?.displayName} + type={CONST.ICON_TYPE_WORKSPACE} + /> + <Text + numberOfLines={1} + style={[styles.flex1, styles.flexGrow1, styles.textStrong]} + > + {personalDetails[item.managerID]?.displayName} + </Text> + </View> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <View style={[styles.flexRow, styles.gap3, styles.flex1, styles.alignItemsCenter]}> + <Avatar + imageStyles={[styles.alignSelfCenter]} + size={CONST.AVATAR_SIZE.SMALL} + source={personalDetails[item.accountID]?.avatar} + name={personalDetails[item.accountID]?.displayName} + type={CONST.ICON_TYPE_WORKSPACE} + /> + <Text + numberOfLines={1} + style={[styles.flex1, styles.flexGrow1, styles.textStrong]} + > + {personalDetails[item.accountID]?.displayName} + </Text> + </View> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.category} + style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.tag} + style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsEnd]}> + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={`${CurrencyUtils.getLocalizedCurrencySymbol(item.currency)}${item.amount}`} + style={[styles.optionDisplayName, styles.textNewKansasNormal, styles.pre, styles.justifyContentCenter]} + /> + </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <Icon + src={Expensicons.CreditCard} + fill={theme.icon} + /> + </View> - styles.justifyContentCenter, - ]} - /> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <Button + success + onPress={() => {}} + small + pressOnEnter + text="View" + /> + </View> </View> - {/* - - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.amount} - style={[ - styles.optionDisplayName, - isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, - styles.sidebarLinkText, - styles.pre, - - styles.justifyContentCenter, - ]} - /> - </View> */} {!!item.rightElement && item.rightElement} </> )} diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 22482e33ab4e..1ae16b2eed66 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -31,14 +31,10 @@ const data = [ transactionID: '1234', modifiedCreated: '2024-05-06 00:00:00', description: 'description description description description', - from: { - displayName: 'TestUser1', - avatarUrl: '', - }, - to: { - displayName: 'TestUser2', - avatarUrl: '', - }, + accountID: '8392101', + managerID: '8392101', + currency: 'USD', + modifiedCurrency: '', category: 'Bananas', tag: 'Green', }, @@ -53,14 +49,10 @@ const data = [ transactionID: '5555', modifiedCreated: '2024-05-06 00:00:00', description: 'description', - from: { - displayName: 'TestUser1', - avatarUrl: '', - }, - to: { - displayName: 'TestUser2', - avatarUrl: '', - }, + accountID: '8392101', + managerID: '8392101', + currency: 'USD', + modifiedCurrency: '', category: 'Bananas', tag: 'Green', }, @@ -77,7 +69,7 @@ function SearchPage({route}: SearchPageProps) { const showMerchantColumn = isSmallScreenWidth && true; return ( - <View style={[styles.flex1, styles.flexRow, styles.justifyContentBetween, styles.pl3]}> + <View style={[styles.flex1, styles.flexRow, styles.justifyContentBetween, styles.pl3, styles.gap3]}> {/* <Text style={styles.searchInputStyle}>{translate('common.receipt')}</Text> */} <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.date')}</Text> {showMerchantColumn && <Text style={[styles.searchInputStyle]}>{translate('common.merchant')}</Text>} @@ -87,6 +79,8 @@ function SearchPage({route}: SearchPageProps) { <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.category')}</Text> <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.tag')}</Text> <Text style={[styles.searchInputStyle, styles.flex1, styles.textAlignRight]}>{translate('common.total')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.type')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.action')}</Text> </View> ); }; diff --git a/src/styles/index.ts b/src/styles/index.ts index 0b994d1780e9..2ddb65004af6 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -438,6 +438,11 @@ const styles = (theme: ThemeColors) => lineHeight: variables.lineHeightHero, }, + textNewKansasNormal: { + fontFamily: FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM, + fontSize: variables.fontSizeNormal, + }, + textStrong: { fontFamily: FontUtils.fontFamily.platform.EXP_NEUE_BOLD, fontWeight: FontUtils.fontWeight.bold, From 9e6a446359464c1947f32580f9f81eee1476b875 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Fri, 26 Apr 2024 17:35:50 +0200 Subject: [PATCH 480/580] Lint fix --- src/pages/iou/request/step/IOURequestStepSendFrom.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx index b61f3d337826..6de3780aa6e8 100644 --- a/src/pages/iou/request/step/IOURequestStepSendFrom.tsx +++ b/src/pages/iou/request/step/IOURequestStepSendFrom.tsx @@ -55,7 +55,7 @@ function IOURequestStepSendFrom({route, transaction, allPolicies}: IOURequestSte ], isSelected: selectedWorkspace?.policyID === policy.id, })); - }, [allPolicies, transaction]); + }, [allPolicies, selectedWorkspace]); const navigateBack = () => { Navigation.goBack(backTo); From 907167cb007b1fa78937b749c828f6bb3e88996e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 17:55:39 +0200 Subject: [PATCH 481/580] improve admin chat searching --- .../report/SystemChatReportFooterMessage.tsx | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/pages/home/report/SystemChatReportFooterMessage.tsx b/src/pages/home/report/SystemChatReportFooterMessage.tsx index a946968f0ec7..c9ccac8f5c18 100644 --- a/src/pages/home/report/SystemChatReportFooterMessage.tsx +++ b/src/pages/home/report/SystemChatReportFooterMessage.tsx @@ -1,6 +1,6 @@ import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Banner from '@components/Banner'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; @@ -15,25 +15,32 @@ import type {OnboardingPurposeType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Policy as PolicyType} from '@src/types/onyx'; type SystemChatReportFooterMessageOnyxProps = { /** Saved onboarding purpose selected by the user */ choice: OnyxEntry<OnboardingPurposeType>; + /** The list of this user's policies */ + policies: OnyxCollection<PolicyType>; + /** policyID for main workspace */ activePolicyID: OnyxEntry<Required<string>>; }; type SystemChatReportFooterMessageProps = SystemChatReportFooterMessageOnyxProps; -function SystemChatReportFooterMessage({choice, activePolicyID}: SystemChatReportFooterMessageProps) { +function SystemChatReportFooterMessage({choice, policies, activePolicyID}: SystemChatReportFooterMessageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const adminChatReport = useMemo(() => { - const policy = PolicyUtils.getPolicy(activePolicyID ?? ''); - return ReportUtils.getReport(String(policy.chatReportIDAdmins)); - }, [activePolicyID]); + const adminPolicy = activePolicyID + ? PolicyUtils.getPolicy(activePolicyID ?? '') + : Object.values(policies ?? {}).find((policy) => PolicyUtils.shouldShowPolicy(policy, false) && policy?.role === CONST.POLICY.ROLE.ADMIN && policy?.chatReportIDAdmins); + + return ReportUtils.getReport(String(adminPolicy?.chatReportIDAdmins)); + }, [activePolicyID, policies]); const content = useMemo(() => { switch (choice) { @@ -74,7 +81,9 @@ export default withOnyx<SystemChatReportFooterMessageProps, SystemChatReportFoot choice: { key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, }, - + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, activePolicyID: { key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, initialValue: null, From 38cbb1b492a3c51cb45ea0101d8fb853da38ae21 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko <rezkiy37@gmail.com> Date: Fri, 26 Apr 2024 17:57:35 +0200 Subject: [PATCH 482/580] attach chatReportIDAdmins to policy optimistically --- src/libs/actions/Policy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 6b8514beab82..c9f62a7a75cf 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -2200,6 +2200,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName errors: {}, }, }, + chatReportIDAdmins: makeMeAdmin ? Number(adminsChatReportID) : undefined, }, }, { From 50b3cb4f2af85a17dd025ab712767b2d01e91f09 Mon Sep 17 00:00:00 2001 From: kmichel <kmichel1030@gmail.com> Date: Fri, 26 Apr 2024 10:11:01 -0700 Subject: [PATCH 483/580] update ref name --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 ++-- .../VideoPlayerContexts/VideoPopoverMenuContext.tsx | 12 ++++++------ src/components/VideoPlayerContexts/types.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index f295c39f410e..d1d7f0ee34da 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -80,7 +80,7 @@ function BaseVideoPlayer({ const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix)); const videoStateRef = useRef<AVPlaybackStatus | null>(null); const {updateVolume} = useVolumeContext(); - const {playerRef} = useVideoPopoverMenuContext(); + const {videoPopoverMenuPlayerRef} = useVideoPopoverMenuContext(); const togglePlayCurrentVideo = useCallback(() => { videoResumeTryNumber.current = 0; @@ -95,7 +95,7 @@ function BaseVideoPlayer({ const showPopoverMenu = (event?: GestureResponderEvent | KeyboardEvent) => { setIsPopoverVisible(true); - playerRef.current = videoPlayerRef.current; + videoPopoverMenuPlayerRef.current = videoPlayerRef.current; if (!event || !('nativeEvent' in event)) { return; } diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx index cd8833039b09..b74a26caa628 100644 --- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx +++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx @@ -1,3 +1,4 @@ +import type {AVPlaybackSourceObject} from 'expo-av'; import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import type {PopoverMenuItem} from '@components/PopoverMenu'; @@ -10,7 +11,6 @@ import CONST from '@src/CONST'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {usePlaybackContext} from './PlaybackContext'; import type {PlaybackSpeed, VideoPopoverMenuContext} from './types'; -import { AVPlaybackSourceObject } from 'expo-av'; const Context = React.createContext<VideoPopoverMenuContext | null>(null); @@ -20,7 +20,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState<PlaybackSpeed>(CONST.VIDEO_PLAYER.PLAYBACK_SPEEDS[2]); const {isOffline} = useNetwork(); const isLocalFile = currentlyPlayingURL && CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => currentlyPlayingURL.startsWith(prefix)); - const playerRef = useRef<VideoWithOnFullScreenUpdate | null>(null); + const videoPopoverMenuPlayerRef = useRef<VideoWithOnFullScreenUpdate | null>(null); const updatePlaybackSpeed = useCallback( (speed: PlaybackSpeed) => { @@ -31,12 +31,12 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { ); const downloadAttachment = useCallback(() => { - if (playerRef.current === null) { + if (videoPopoverMenuPlayerRef.current === null) { return; } - const sourceURI = addEncryptedAuthTokenToURL((playerRef.current.props.source as AVPlaybackSourceObject).uri); + const sourceURI = addEncryptedAuthTokenToURL((videoPopoverMenuPlayerRef.current.props.source as AVPlaybackSourceObject).uri); fileDownload(sourceURI); - }, [playerRef]); + }, [videoPopoverMenuPlayerRef]); const menuItems = useMemo(() => { const items: PopoverMenuItem[] = []; @@ -66,7 +66,7 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { return items; }, [currentPlaybackSpeed, downloadAttachment, translate, updatePlaybackSpeed, isOffline, isLocalFile]); - const contextValue = useMemo(() => ({menuItems, playerRef, updatePlaybackSpeed}), [menuItems, playerRef, updatePlaybackSpeed]); + const contextValue = useMemo(() => ({menuItems, videoPopoverMenuPlayerRef, updatePlaybackSpeed}), [menuItems, videoPopoverMenuPlayerRef, updatePlaybackSpeed]); return <Context.Provider value={contextValue}>{children}</Context.Provider>; } diff --git a/src/components/VideoPlayerContexts/types.ts b/src/components/VideoPlayerContexts/types.ts index 6323c83d6d9e..ea09281d7676 100644 --- a/src/components/VideoPlayerContexts/types.ts +++ b/src/components/VideoPlayerContexts/types.ts @@ -28,7 +28,7 @@ type VolumeContext = { type VideoPopoverMenuContext = { menuItems: PopoverMenuItem[]; - playerRef: MutableRefObject<VideoWithOnFullScreenUpdate | null>; + videoPopoverMenuPlayerRef: MutableRefObject<VideoWithOnFullScreenUpdate | null>; updatePlaybackSpeed: (speed: PlaybackSpeed) => void; }; From 133e0c00695db7797ccf42f6e4375ee7f3ccc978 Mon Sep 17 00:00:00 2001 From: Aldo Canepa <aldo@expensify.com> Date: Fri, 26 Apr 2024 12:05:21 -0700 Subject: [PATCH 484/580] Update patch version --- ...2.patch => @expensify+react-native-live-markdown+0.1.64.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/{@expensify+react-native-live-markdown+0.1.62.patch => @expensify+react-native-live-markdown+0.1.64.patch} (100%) diff --git a/patches/@expensify+react-native-live-markdown+0.1.62.patch b/patches/@expensify+react-native-live-markdown+0.1.64.patch similarity index 100% rename from patches/@expensify+react-native-live-markdown+0.1.62.patch rename to patches/@expensify+react-native-live-markdown+0.1.64.patch From 9b4da0dbcdef19cb8296ecdbff4ffeda45243045 Mon Sep 17 00:00:00 2001 From: Anusha <anusharukhsar1@gmail.com> Date: Sat, 27 Apr 2024 00:11:42 +0500 Subject: [PATCH 485/580] Refactored the code --- .../ReportActionItem/MoneyRequestView.tsx | 11 ++++---- src/libs/ReportUtils.ts | 28 ++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 247ed91a961c..c662198abc74 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -194,11 +194,12 @@ function MoneyRequestView({ const distance = DistanceRequestUtils.convertToDistanceInMeters((transaction?.comment?.customUnit?.quantity as number) ?? 0, unit); const rateToDisplay = DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); - const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); - const getMerchant = isEmptyMerchant ? '' : transactionMerchant; - const getAmount = formattedTransactionAmount ? formattedTransactionAmount.toString() : ''; - const merchantTitle = isScanning ? translate('iou.receiptStatusTitle') : getMerchant; - const amountTitle = isScanning ? translate('iou.receiptStatusTitle') : getAmount; + let merchantTitle = isEmptyMerchant ? '' : transactionMerchant; + let amountTitle = formattedTransactionAmount ? formattedTransactionAmount.toString() : ''; + if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { + merchantTitle = translate('iou.receiptStatusTitle'); + amountTitle = translate('iou.receiptStatusTitle'); + } const saveBillable = useCallback( (newBillable: boolean) => { // If the value hasn't changed, don't request to save changes on the server and just close the modal diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 16f11ef02857..8fdb6facda32 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2636,27 +2636,29 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis return Localize.translateLocal('iou.receiptMissingDetails'); } - if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { - const transactionDetails = getTransactionDetails(transaction); - return Localize.translateLocal('iou.threadTrackReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', - comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', - }); - } - if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { return Localize.translateLocal('iou.fieldPending'); } const transactionDetails = getTransactionDetails(transaction); - return Localize.translateLocal( - ReportActionsUtils.isSentMoneyReportAction(reportAction) && !ReportActionsUtils.isTrackExpenseAction(reportAction) ? 'iou.threadPaySomeoneReportName' : 'iou.threadExpenseReportName', - { + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? ''; + const comment = (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? ''; + if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { + return Localize.translateLocal('iou.threadTrackReportName', {formattedAmount, comment}); + } + if (ReportActionsUtils.isSentMoneyReportAction(reportAction)) { + return Localize.translateLocal('iou.threadPaySomeoneReportName', {formattedAmount, comment}); + } + + if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { + const transactionDetails = getTransactionDetails(transaction); + return Localize.translateLocal('iou.threadTrackReportName', { formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', - }, - ); + }); + } + return Localize.translateLocal('iou.threadExpenseReportName', {formattedAmount, comment}); } /** From 714170395685a5c8f6d75c9b285fb99f62fc0e5f Mon Sep 17 00:00:00 2001 From: Anusha <anusharukhsar1@gmail.com> Date: Sat, 27 Apr 2024 00:23:41 +0500 Subject: [PATCH 486/580] link fix --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 536f0edf59fb..5bdf21c87824 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2652,7 +2652,6 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis } if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { - const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal('iou.threadTrackReportName', { formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', From 4e8fd363dcd982baf5a7776073d5ebfff8c091c9 Mon Sep 17 00:00:00 2001 From: Robert Kozik <robert.kozik@swmansion.com> Date: Fri, 26 Apr 2024 21:53:47 +0200 Subject: [PATCH 487/580] run prettier --- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 95e3c038dfc0..8c0262932344 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -44,6 +44,7 @@ import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutsi import type {ComposerRef, SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; +import variables from '@styles/variables'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; import * as Report from '@userActions/Report'; @@ -52,7 +53,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import variables from '@styles/variables'; type SyncSelection = { position: number; From f72d330dbf47e244f6fced3ac0a5d334fbfff029 Mon Sep 17 00:00:00 2001 From: Aldo Canepa <aldo@expensify.com> Date: Fri, 26 Apr 2024 13:05:57 -0700 Subject: [PATCH 488/580] Upgrade expensify/react-native-live-markdown and remove patch --- ios/Podfile.lock | 60 +++++++++---------- package-lock.json | 8 +-- package.json | 2 +- ...fy+react-native-live-markdown+0.1.64.patch | 9 --- 4 files changed, 35 insertions(+), 44 deletions(-) delete mode 100644 patches/@expensify+react-native-live-markdown+0.1.64.patch diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6ef622bba722..98f18dbe2645 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1816,7 +1816,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.64): + - RNLiveMarkdown (0.1.69): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -1834,9 +1834,9 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/common (= 0.1.64) + - RNLiveMarkdown/common (= 0.1.69) - Yoga - - RNLiveMarkdown/common (0.1.64): + - RNLiveMarkdown/common (0.1.69): - glog - hermes-engine - RCT-Folly (= 2022.05.16.00) @@ -2454,14 +2454,14 @@ SPEC CHECKSUMS: libvmaf: 27f523f1e63c694d14d534cd0fddd2fab0ae8711 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 lottie-ios: 3d98679b41fa6fd6aff2352b3953dbd3df8a397e - lottie-react-native: 80bda323805fa62005afff0583d2927a89108f20 + lottie-react-native: d0e530160e1a0116ab567343d843033c496d0d97 MapboxCommon: 20466d839cc43381c44df09d19f7f794b55b9a93 MapboxCoreMaps: c21f433decbb295874f0c2464e492166db813b56 MapboxMaps: c3b36646b9038706bbceb5de203bcdd0f411e9d0 MapboxMobileEvents: de50b3a4de180dd129c326e09cd12c8adaaa46d6 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 Onfido: 342cbecd7a4383e98dfe7f9c35e98aaece599062 - onfido-react-native-sdk: 81e930e77236a0fc3da90e6a6eb834734d8ec2f5 + onfido-react-native-sdk: 3e3b0dd70afa97410fb318d54c6a415137968ef2 Plaid: 7829e84db6d766a751c91a402702946d2977ddcb PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 @@ -2486,26 +2486,26 @@ SPEC CHECKSUMS: React-jsitracing: e8a2dafb9878dbcad02b6b2b88e66267fb427b74 React-logger: 0a57b68dd2aec7ff738195f081f0520724b35dab React-Mapbuffer: 63913773ed7f96b814a2521e13e6d010282096ad - react-native-airship: 6ab7a7974d53f92b0c46548fc198f797fdbf371f - react-native-blob-util: a3ee23cfdde79c769c138d505670055de233b07a - react-native-cameraroll: 95ce0d1a7d2d1fe55bf627ab806b64de6c3e69e9 + react-native-airship: 38e2596999242b68c933959d6145512e77937ac0 + react-native-blob-util: 1ddace5234c62e3e6e4e154d305ad07ef686599b + react-native-cameraroll: f373bebbe9f6b7c3fd2a6f97c5171cda574cf957 react-native-config: 5ce986133b07fc258828b20b9506de0e683efc1c react-native-document-picker: 8532b8af7c2c930f9e202aac484ac785b0f4f809 - react-native-geolocation: c1c21a8cda4abae6724a322458f64ac6889b8c2b + react-native-geolocation: f9e92eb774cb30ac1e099f34b3a94f03b4db7eb3 react-native-image-picker: f8a13ff106bcc7eb00c71ce11fdc36aac2a44440 - react-native-key-command: 74d18ad516037536c2f671ef0914bcce7739b2f5 + react-native-key-command: 28ccfa09520e7d7e30739480dea4df003493bfe8 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d - react-native-netinfo: 6479e7e2198f936e5abc14a3ec4d469ccbaf81e2 - react-native-pager-view: 9ac6bc0fb3fa31c6d403b253ee361e62ff7ccf7f - react-native-pdf: cd256a00b9d65cb1008dcca2792d7bfb8874838d - react-native-performance: 1aa5960d005159f4ab20be15b44714b53b44e075 - react-native-plaid-link-sdk: 93870f8cd1de8e0acca5cb5020188bdc94e15db6 + react-native-netinfo: 02d31de0e08ab043d48f2a1a8baade109d7b6ca5 + react-native-pager-view: ccd4bbf9fc7effaf8f91f8dae43389844d9ef9fa + react-native-pdf: 762369633665ec02ac227aefe2f4558b92475c23 + react-native-performance: fb21ff0c9bd7a10789c69d948f25b0067d29f7a9 + react-native-plaid-link-sdk: 2a91ef7e257ae16d180a1ca14ba3041ae0836fbf react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 - react-native-release-profiler: 42fc8e09b4f6f9b7d14cc5a15c72165e871c0918 + react-native-release-profiler: 14ccdc0eeb03bedf625cf68d53d80275a81b19dd react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c - react-native-safe-area-context: e8bdd57d9f8d34cc336f0ee6acb30712a8454446 + react-native-safe-area-context: 9d79895b60b8be151fdf6faef9d2d0591eeecc63 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 - react-native-webview: a5f5f316527235f869992aaaf05050776198806d + react-native-webview: f8ab7a37905b2366a3e849ce5992b9724f6a528d React-nativeconfig: d7af5bae6da70fa15ce44f045621cf99ed24087c React-NativeModulesApple: 0123905d5699853ac68519607555a9a4f5c7b3ac React-perflogger: 8a1e1af5733004bdd91258dcefbde21e0d1faccd @@ -2530,35 +2530,35 @@ SPEC CHECKSUMS: React-utils: 6e5ad394416482ae21831050928ae27348f83487 ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522 RNAppleAuthentication: 0571c08da8c327ae2afc0261b48b4a515b0286a6 - RNCClipboard: c73bbc2e9012120161f1012578418827983bfd0c - RNCPicker: c77efa39690952647b83d8085520bf50ebf94ecb - RNDeviceInfo: cbf78fdb515ae73e641ee7c6b474f77a0299e7e6 + RNCClipboard: 081418ae3b391b1012c3f41d045e5e39f1beed71 + RNCPicker: a37026a67de0cf1a33ffe8722783527e3b18ea9f + RNDeviceInfo: 449272e9faf2afe94a3fe2896d169e92277fffa8 RNDevMenu: 72807568fe4188bd4c40ce32675d82434b43c45d RNFBAnalytics: f76bfa164ac235b00505deb9fc1776634056898c RNFBApp: 729c0666395b1953198dc4a1ec6deb8fbe1c302e RNFBCrashlytics: 2061ca863e8e2fa1aae9b12477d7dfa8e88ca0f9 RNFBPerf: 389914cda4000fe0d996a752532a591132cbf3f9 - RNFlashList: 5b0e8311e4cf1ad91e410fd7c8526a89fb5826d1 + RNFlashList: 76c2fab003330924ab1a140d13aadf3834dc32e0 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 1190c218cdaaf029ee1437076a3fbbc3297d89fb + RNGestureHandler: 74b7b3d06d667ba0bbf41da7718f2607ae0dfe8f RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: ddc8b2d827febd397c88137ffc7a6e102d511b8b + RNLiveMarkdown: bfabd5938e5af5afc1e60e4e34286b17f8308184 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 - rnmapbox-maps: 3e273e0e867a079ec33df9ee33bb0482434b897d - RNPermissions: 8990fc2c10da3640938e6db1647cb6416095b729 + rnmapbox-maps: 51aee278cc2af8af9298f91a2aad7210739785b4 + RNPermissions: 0b61d30d21acbeafe25baaa47d9bae40a0c65216 RNReactNativeHapticFeedback: 616c35bdec7d20d4c524a7949ca9829c09e35f37 - RNReanimated: 605409e0d0ced6f2e194ae585fedc2f8a1935bf2 - RNScreens: 65a936f4e227b91e4a8e2a7d4c4607355bfefda0 + RNReanimated: 51db0fff543694d931bd3b7cab1a3b36bd86c738 + RNScreens: 9ec969a95987a6caae170ef09313138abf3331e1 RNShare: 2a4cdfc0626ad56b0ef583d424f2038f772afe58 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 - RNSVG: db32cfcad0a221fd175e0882eff7bcba7690380a + RNSVG: 18f1381e046be2f1c30b4724db8d0c966238089f SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 SDWebImageAVIFCoder: 8348fef6d0ec69e129c66c9fe4d74fbfbf366112 SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 - VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf + VisionCamera: 1394a316c7add37e619c48d7aa40b38b954bf055 Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d diff --git a/package-lock.json b/package-lock.json index 5d91baef8a44..2e4c6fc0d80f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.64", + "@expensify/react-native-live-markdown": "0.1.69", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", @@ -3568,9 +3568,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.64", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.64.tgz", - "integrity": "sha512-X6NXYH420wC+BFNOuzJflpegwSKTiuzLvbDeehCpxrtS059Eyb2FbwkzrAVH7TGwDeghFgaQfY9rVkSCGUAbsw==", + "version": "0.1.69", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.69.tgz", + "integrity": "sha512-ZJG6f06lHrNb0s/92JyyvsSDGGZLdU/a/YXir2A5UFCiERVWkgJxcugsYbEMemh2HsWD6GXvhq1Sngj2H620nw==", "engines": { "node": ">= 18.0.0" }, diff --git a/package.json b/package.json index fdb6155e24b7..551f38235a98 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.64", + "@expensify/react-native-live-markdown": "0.1.69", "@expo/metro-runtime": "~3.1.1", "@formatjs/intl-datetimeformat": "^6.10.0", "@formatjs/intl-listformat": "^7.2.2", diff --git a/patches/@expensify+react-native-live-markdown+0.1.64.patch b/patches/@expensify+react-native-live-markdown+0.1.64.patch deleted file mode 100644 index 0d2c962efd0f..000000000000 --- a/patches/@expensify+react-native-live-markdown+0.1.64.patch +++ /dev/null @@ -1,9 +0,0 @@ -diff --git a/node_modules/@expensify/react-native-live-markdown/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m b/node_modules/@expensify/react-native-live-markdown/ios/RCTBackedTextFieldDelegateAdapter+Markdown.mm -similarity index 100% -rename from node_modules/@expensify/react-native-live-markdown/ios/RCTBackedTextFieldDelegateAdapter+Markdown.m -rename to node_modules/@expensify/react-native-live-markdown/ios/RCTBackedTextFieldDelegateAdapter+Markdown.mm -diff --git a/node_modules/@expensify/react-native-live-markdown/ios/RCTBaseTextInputView+Markdown.m b/node_modules/@expensify/react-native-live-markdown/ios/RCTBaseTextInputView+Markdown.mm -similarity index 100% -rename from node_modules/@expensify/react-native-live-markdown/ios/RCTBaseTextInputView+Markdown.m -rename to node_modules/@expensify/react-native-live-markdown/ios/RCTBaseTextInputView+Markdown.mm - From b7197a62e4c788c4a8b40ea71c1fb19aaff3df5c Mon Sep 17 00:00:00 2001 From: Aldo Canepa <aldo@expensify.com> Date: Fri, 26 Apr 2024 13:54:45 -0700 Subject: [PATCH 489/580] Fix country crash --- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index c3515c393123..d8edc96d3d05 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -102,7 +102,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro titleTranslationKey: 'workspace.moreFeatures.taxes.title', subtitleTranslationKey: 'workspace.moreFeatures.taxes.subtitle', isActive: (policy?.tax?.trackingEnabled ?? false) || isSyncTaxEnabled, - disabled: isSyncTaxEnabled || policy?.connections?.quickbooksOnline.data.country === CONST.COUNTRY.US, + disabled: isSyncTaxEnabled || policy?.connections?.quickbooksOnline?.data?.country === CONST.COUNTRY.US, pendingAction: policy?.pendingFields?.tax, action: (isEnabled: boolean) => { Policy.enablePolicyTaxes(policy?.id ?? '', isEnabled); From 396447f1b601e7eb29063542dd7e1b74b2a86fc0 Mon Sep 17 00:00:00 2001 From: Cristi Paval <cristi@expensify.com> Date: Fri, 26 Apr 2024 22:57:35 +0200 Subject: [PATCH 490/580] Fix TS issues --- .../ReportActionCompose/AttachmentPickerWithMenuItems.tsx | 5 +++++ src/pages/iou/request/IOURequestStartPage.tsx | 1 + 2 files changed, 6 insertions(+) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 1adb161a92e9..924d9c5f1cd9 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -147,6 +147,11 @@ function AttachmentPickerWithMenuItems({ text: translate('iou.trackExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK, report?.reportID ?? ''), }, + [CONST.IOU.TYPE.INVOICE]: { + icon: Expensicons.Invoice, + text: translate('workspace.invoices.sendInvoice'), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.INVOICE, report?.reportID ?? ''), + }, }; return ReportUtils.temporary_getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ diff --git a/src/pages/iou/request/IOURequestStartPage.tsx b/src/pages/iou/request/IOURequestStartPage.tsx index 95c7b09ce1c1..db58e4220cba 100644 --- a/src/pages/iou/request/IOURequestStartPage.tsx +++ b/src/pages/iou/request/IOURequestStartPage.tsx @@ -67,6 +67,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.PAY]: translate('iou.paySomeone', {name: ReportUtils.getPayeeName(report)}), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitExpense'), [CONST.IOU.TYPE.TRACK]: translate('iou.trackExpense'), + [CONST.IOU.TYPE.INVOICE]: translate('workspace.invoices.sendInvoice'), }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const {canUseP2PDistanceRequests} = usePermissions(iouType); From 2be93d7304382eba28e197571e51dd531ad9a9ad Mon Sep 17 00:00:00 2001 From: Janic Duplessis <janicduplessis@gmail.com> Date: Fri, 26 Apr 2024 17:13:37 -0400 Subject: [PATCH 491/580] Update react-native-quick-sqlite --- ios/Podfile.lock | 26 ++++++++++++++----- package-lock.json | 6 +++-- package.json | 2 +- ...act-native-quick-sqlite+8.0.0-beta.2.patch | 12 --------- 4 files changed, 25 insertions(+), 21 deletions(-) delete mode 100644 patches/react-native-quick-sqlite+8.0.0-beta.2.patch diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6ef622bba722..0398bd3b1324 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1380,10 +1380,25 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-quick-sqlite (8.0.0-beta.2): - - React - - React-callinvoker + - react-native-quick-sqlite (8.0.6): + - glog + - hermes-engine + - RCT-Folly (= 2022.05.16.00) + - RCTRequired + - RCTTypeSafety + - React-Codegen - React-Core + - React-debug + - React-Fabric + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-release-profiler (0.1.6): - glog - hermes-engine @@ -2062,7 +2077,6 @@ DEPENDENCIES: - ExpoImageManipulator (from `../node_modules/expo-image-manipulator/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - libevent (~> 2.1.12) @@ -2500,7 +2514,7 @@ SPEC CHECKSUMS: react-native-pdf: cd256a00b9d65cb1008dcca2792d7bfb8874838d react-native-performance: 1aa5960d005159f4ab20be15b44714b53b44e075 react-native-plaid-link-sdk: 93870f8cd1de8e0acca5cb5020188bdc94e15db6 - react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 + react-native-quick-sqlite: f7b9f578b8b3b608dc742240b0103faae5b61f63 react-native-release-profiler: 42fc8e09b4f6f9b7d14cc5a15c72165e871c0918 react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: e8bdd57d9f8d34cc336f0ee6acb30712a8454446 @@ -2558,7 +2572,7 @@ SPEC CHECKSUMS: SDWebImageWebPCoder: af09429398d99d524cae2fe00f6f0f6e491ed102 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 - VisionCamera: 3033e0dd5272d46e97bcb406adea4ae0e6907abf + VisionCamera: 8c5c9c50b3d76018782a823cee2f0b8b628c8604 Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70 PODFILE CHECKSUM: a25a81f2b50270f0c0bd0aff2e2ebe4d0b4ec06d diff --git a/package-lock.json b/package-lock.json index 5d91baef8a44..47c916b0cce1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,7 +108,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf", "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", - "react-native-quick-sqlite": "^8.0.0-beta.2", + "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", "react-native-reanimated": "^3.7.2", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", @@ -31432,7 +31432,9 @@ } }, "node_modules/react-native-quick-sqlite": { - "version": "8.0.0-beta.2", + "version": "8.0.6", + "resolved": "git+ssh://git@github.com/margelo/react-native-quick-sqlite.git#abc91857d4b3efb2020ec43abd2a508563b64316", + "integrity": "sha512-/tBM6Oh8ye3d+hIhURRA9hlBausKqQmscgyt4ZcKluPjBti0bgLb0cyL8Gyd0cbCakaVgym25VyGjaeicV/01A==", "license": "MIT", "peerDependencies": { "react": "*", diff --git a/package.json b/package.json index fdb6155e24b7..d01e4bdbf554 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "react-native-picker-select": "git+https://github.com/Expensify/react-native-picker-select.git#da50d2c5c54e268499047f9cc98b8df4196c1ddf", "react-native-plaid-link-sdk": "11.5.0", "react-native-qrcode-svg": "^6.2.0", - "react-native-quick-sqlite": "^8.0.0-beta.2", + "react-native-quick-sqlite": "git+https://github.com/margelo/react-native-quick-sqlite#abc91857d4b3efb2020ec43abd2a508563b64316", "react-native-reanimated": "^3.7.2", "react-native-release-profiler": "^0.1.6", "react-native-render-html": "6.3.1", diff --git a/patches/react-native-quick-sqlite+8.0.0-beta.2.patch b/patches/react-native-quick-sqlite+8.0.0-beta.2.patch deleted file mode 100644 index b5810c903873..000000000000 --- a/patches/react-native-quick-sqlite+8.0.0-beta.2.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/node_modules/react-native-quick-sqlite/android/build.gradle b/node_modules/react-native-quick-sqlite/android/build.gradle -index 323d34e..c2d0c44 100644 ---- a/node_modules/react-native-quick-sqlite/android/build.gradle -+++ b/node_modules/react-native-quick-sqlite/android/build.gradle -@@ -90,7 +90,6 @@ android { - externalNativeBuild { - cmake { - cppFlags "-O2", "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID" -- abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' - arguments '-DANDROID_STL=c++_shared', - "-DREACT_NATIVE_DIR=${REACT_NATIVE_DIR}", - "-DSQLITE_FLAGS='${SQLITE_FLAGS ? SQLITE_FLAGS : ''}'" From 814853e15a7f0a1f964ad8242f3eb6c4f3d79863 Mon Sep 17 00:00:00 2001 From: Anusha <anusharukhsar1@gmail.com> Date: Sat, 27 Apr 2024 03:03:45 +0500 Subject: [PATCH 492/580] remove dupe code --- src/libs/ReportUtils.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5bdf21c87824..e3719ab93a21 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2650,13 +2650,6 @@ function getTransactionReportName(reportAction: OnyxEntry<ReportAction | Optimis if (ReportActionsUtils.isSentMoneyReportAction(reportAction)) { return Localize.translateLocal('iou.threadPaySomeoneReportName', {formattedAmount, comment}); } - - if (ReportActionsUtils.isTrackExpenseAction(reportAction)) { - return Localize.translateLocal('iou.threadTrackReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', - comment: (!TransactionUtils.isMerchantMissing(transaction) ? transactionDetails?.merchant : transactionDetails?.comment) ?? '', - }); - } return Localize.translateLocal('iou.threadExpenseReportName', {formattedAmount, comment}); } From 3a4e053e361ae56c46179ff14cbf941efeb8bc08 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Fri, 26 Apr 2024 22:48:30 +0000 Subject: [PATCH 493/580] Update version to 1.4.67-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- ios/NotificationServiceExtension/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2d690ed72b8c..d5afad11a9fa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046605 - versionName "1.4.66-5" + versionCode 1001046700 + versionName "1.4.67-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 42b54741d190..4d2e7ba3b992 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> - <string>1.4.66</string> + <string>1.4.67</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleURLTypes</key> @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.66.5</string> + <string>1.4.67.0</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f1986986a1a8..952fbeddd75e 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ <key>CFBundlePackageType</key> <string>BNDL</string> <key>CFBundleShortVersionString</key> - <string>1.4.66</string> + <string>1.4.67</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.66.5</string> + <string>1.4.67.0</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index de4dba072fbb..d6920d746496 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ <key>CFBundleName</key> <string>$(PRODUCT_NAME)</string> <key>CFBundleShortVersionString</key> - <string>1.4.66</string> + <string>1.4.67</string> <key>CFBundleVersion</key> - <string>1.4.66.5</string> + <string>1.4.67.0</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index 5d91baef8a44..38d9c8c68037 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.66-5", + "version": "1.4.67-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.66-5", + "version": "1.4.67-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index fdb6155e24b7..5c6588c143fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.66-5", + "version": "1.4.67-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 37999e80191c469f12df50a82f2c3d0900f32b3d Mon Sep 17 00:00:00 2001 From: Rayane Djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sat, 27 Apr 2024 00:55:13 +0100 Subject: [PATCH 494/580] Improve workflow failiure notifier - Add logic to fetch and process previous workflow run and jobs --- .github/workflows/failureNotifier.yml | 47 +++++++++++++++++++++------ 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 9eb5bc6eb409..7db1490523f4 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -27,19 +27,52 @@ jobs: }); return jobsData.data; + - name: Fetch Previous Workflow Run + id: previous-workflow-run + uses: actions/github-script@v7 + with: + script: | + const runId = "${{ github.event.workflow_run.id }}"; + const allRuns = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: preDeploy.yml, + }); + const run = allRuns.data.workflow_runs.find(run => run.id === runId); + const run_number = run.run_number; + const previousRun = allRuns.data.workflow_runs.find(run => run.run_number === run_number - 1); + if (previousRun.actor.login === 'OSBotify') { + return allRuns.data.workflow_runs.find(run.run_number === run_number - 2); + } + return previousRun; + + - name: Fetch Previous Workflow Run Jobs + id: previous-workflow-jobs + uses: actions/github-script@v7 + with: + script: | + const previousRun = ${{ steps.previous-workflow-run.outputs.result }}; + const runId = previousRun.id; + const jobsData = await github.rest.actions.listJobsForWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + return jobsData.data; + - name: Process Each Failed Job uses: actions/github-script@v7 with: script: | const jobs = ${{ steps.fetch-workflow-jobs.outputs.result }}; - + const previousRun = ${{ steps.previous-workflow-run.outputs.result }}; + const previousRunJobs = ${{ steps.previous-workflow-jobs.outputs.result }}; const headCommit = "${{ github.event.workflow_run.head_commit.id }}"; const prData = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, repo: context.repo.repo, commit_sha: headCommit, }); - const pr = prData.data[0]; const prLink = pr.html_url; const prAuthor = pr.user.login; @@ -50,14 +83,8 @@ jobs: if (jobs.jobs[i].conclusion == 'failure') { const jobName = jobs.jobs[i].name; const jobLink = jobs.jobs[i].html_url; - const issues = await github.rest.issues.listForRepo({ - owner: context.repo.owner, - repo: context.repo.repo, - labels: failureLabel, - state: 'open' - }); - const existingIssue = issues.data.find(issue => issue.title.includes(jobName)); - if (!existingIssue) { + const previousJobSucceeded = previousRunJobs.jobs.find(job => job.name === jobName && job.conclusion === 'success'); + if (previousJobSucceeded) { const annotations = await github.rest.checks.listAnnotations({ owner: context.repo.owner, repo: context.repo.repo, From cad58172a84d6e490cf7f22c79c6d72f467b827e Mon Sep 17 00:00:00 2001 From: dukenv0307 <dukenv0307@gmail.com> Date: Sat, 27 Apr 2024 16:50:49 +0700 Subject: [PATCH 495/580] add empty line --- src/types/onyx/Report.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 79e4b40fbb7e..30984e99eb94 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -138,6 +138,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< displayName?: string; lastMessageHtml?: string; lastActorAccountID?: number; + // indicate the type of the last action lastActionType?: ValueOf<typeof CONST.REPORT.ACTIONS.TYPE>; ownerAccountID?: number; @@ -152,6 +153,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< managerEmail?: string; parentReportActionIDs?: number[]; errorFields?: OnyxCommon.ErrorFields; + /** Whether the report is waiting on a bank account */ isWaitingOnBankAccount?: boolean; From 642d7e095c542592b851e82433a9fc0f4d9f1dfb Mon Sep 17 00:00:00 2001 From: dukenv0307 <129500732+dukenv0307@users.noreply.github.com> Date: Sat, 27 Apr 2024 17:07:21 +0700 Subject: [PATCH 496/580] Update src/types/onyx/Report.ts Co-authored-by: DylanDylann <141406735+DylanDylann@users.noreply.github.com> --- src/types/onyx/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 30984e99eb94..1848afca25da 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -139,7 +139,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< lastMessageHtml?: string; lastActorAccountID?: number; - // indicate the type of the last action + // The type of the last action lastActionType?: ValueOf<typeof CONST.REPORT.ACTIONS.TYPE>; ownerAccountID?: number; ownerEmail?: string; From 43915b336be07772d1f835c5865f56769d2a0d5d Mon Sep 17 00:00:00 2001 From: Janic Duplessis <janicduplessis@gmail.com> Date: Sat, 27 Apr 2024 15:43:38 -0400 Subject: [PATCH 497/580] Add isFocused check --- src/pages/home/report/ReportActionsView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index e58e0fa3312e..cb904327e625 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -354,6 +354,7 @@ function ReportActionsView({ if ( !reportActionID || + !isFocused || isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || @@ -375,6 +376,7 @@ function ReportActionsView({ network.isOffline, reportActions.length, newestReportAction, + isFocused, ]); /** From bd11681872ba645380e0cfe9d7e4ea6d1c0ffff5 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Sun, 28 Apr 2024 09:33:35 -0500 Subject: [PATCH 498/580] Update docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md Spelling tweak Co-authored-by: Amy Evans <amy@expensify.com> --- .../reports/Automatically-submit-employee-reports.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md b/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md index ffe5f1cf2273..49b5bd522464 100644 --- a/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md +++ b/docs/articles/expensify-classic/reports/Automatically-submit-employee-reports.md @@ -18,7 +18,7 @@ If an expense has a violation, Scheduled Submit will not automatically submit it 2. Click the **Group** tab on the left (or click the Individual tab to enable Scheduled Submit for your individual workspace). 3. Click the desired workspace name. 4. Click the **Reports** tab on the left. -5. Click the Schedule Submit toggle to enable it. +5. Click the Scheduled Submit toggle to enable it. 6. Click the “How often expenses submit” dropdown and select the submission schedule: - **Daily**: Expenses are submitted every evening. Expenses with violations are submitted the day after the violations are corrected. - **Weekly**: Expenses are submitted once a week. Expenses with violations are submitted the following Sunday after the violations are corrected. From eb0e211688a59471411bf0a46e3dfa5944af3acc Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Sun, 28 Apr 2024 09:34:24 -0500 Subject: [PATCH 499/580] Update docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md Grammatical fix Co-authored-by: Amy Evans <amy@expensify.com> --- .../reports/Assign-tag-and-category-approvers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md b/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md index a831d770160c..9467c07d95ba 100644 --- a/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md +++ b/docs/articles/expensify-classic/reports/Assign-tag-and-category-approvers.md @@ -30,6 +30,6 @@ Tag approvers are only supported for a single level of tags, not for multi-level 2. Click the **Group** tab on the left. 3. Click the desired workspace name. 4. Click the **Tags** tab on the left. -5. Locate the tags list In the list of tags and click the Approver field to assign an approver. +5. Locate the tag in the list of tags and click the Approver field to assign an approver. </div> From 23c48b26e7e4bfcd041bc75bbab755d25e05f65e Mon Sep 17 00:00:00 2001 From: Rayane Djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:12:50 +0100 Subject: [PATCH 500/580] fixes --- .github/workflows/failureNotifier.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index 7db1490523f4..ab120943c863 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -32,7 +32,7 @@ jobs: uses: actions/github-script@v7 with: script: | - const runId = "${{ github.event.workflow_run.id }}"; + const runId = ${{ github.event.workflow_run.id }}; const allRuns = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, @@ -83,7 +83,8 @@ jobs: if (jobs.jobs[i].conclusion == 'failure') { const jobName = jobs.jobs[i].name; const jobLink = jobs.jobs[i].html_url; - const previousJobSucceeded = previousRunJobs.jobs.find(job => job.name === jobName && job.conclusion === 'success'); + const previousJob = previousRunJobs.jobs.find(job => job.name === jobName); + previousJobSucceeded = previousJob.conclusion === 'success'; if (previousJobSucceeded) { const annotations = await github.rest.checks.listAnnotations({ owner: context.repo.owner, From 7e4c70dff9648d34841583bed8b5a7985648aa1e Mon Sep 17 00:00:00 2001 From: Rayane Djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:15:26 +0100 Subject: [PATCH 501/580] fixes --- .github/workflows/failureNotifier.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index ab120943c863..d10636041ac2 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -36,13 +36,13 @@ jobs: const allRuns = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, - workflow_id: preDeploy.yml, + workflow_id: 'preDeploy.yml', }); const run = allRuns.data.workflow_runs.find(run => run.id === runId); const run_number = run.run_number; const previousRun = allRuns.data.workflow_runs.find(run => run.run_number === run_number - 1); if (previousRun.actor.login === 'OSBotify') { - return allRuns.data.workflow_runs.find(run.run_number === run_number - 2); + return allRuns.data.workflow_runs.find(run => run.run_number === run_number - 2); } return previousRun; From c1eeff7c8bb74d077f0a3dab15df487b070d68b1 Mon Sep 17 00:00:00 2001 From: Rayane Djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:41:49 +0100 Subject: [PATCH 502/580] fix mocks --- workflow_tests/mocks/failureNotifierMocks.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/workflow_tests/mocks/failureNotifierMocks.ts b/workflow_tests/mocks/failureNotifierMocks.ts index cbea6fce95ae..72a79d9ff17b 100644 --- a/workflow_tests/mocks/failureNotifierMocks.ts +++ b/workflow_tests/mocks/failureNotifierMocks.ts @@ -5,8 +5,16 @@ import {createMockStep} from '../utils/utils'; // notifyfailure const FAILURENOTIFIER__NOTIFYFAILURE__FETCH_WORKFLOW_RUN_JOBS__STEP_MOCK = createMockStep('Fetch Workflow Run Jobs', 'Fetch Workflow Run Jobs', 'NOTIFYFAILURE', [], []); +const FAILURENOTIFIER__NOTIFYFAILURE_FETCH_PREVIOUS_WORKFLOW_RUN__STEP_MOCK = createMockStep('Fetch Previous Workflow Run', 'Fetch Previous Workflow Run', 'NOTIFYFAILURE', [], []); +const FAILURENOTIFIER__NOTIFYFAILURE_FETCH_PREVIOUS_WORKFLOW_RUN_JOBS__STEP_MOCK = createMockStep('Fetch Previous Workflow Run Jobs', 'Fetch Previous Workflow Run Jobs', 'NOTIFYFAILURE', [], []); const FAILURENOTIFIER__NOTIFYFAILURE__PROCESS_EACH_FAILED_JOB__STEP_MOCK = createMockStep('Process Each Failed Job', 'Process Each Failed Job', 'NOTIFYFAILURE', [], []); -const FAILURENOTIFIER__NOTIFYFAILURE__STEP_MOCKS = [FAILURENOTIFIER__NOTIFYFAILURE__FETCH_WORKFLOW_RUN_JOBS__STEP_MOCK, FAILURENOTIFIER__NOTIFYFAILURE__PROCESS_EACH_FAILED_JOB__STEP_MOCK]; + +const FAILURENOTIFIER__NOTIFYFAILURE__STEP_MOCKS = [ + FAILURENOTIFIER__NOTIFYFAILURE__FETCH_WORKFLOW_RUN_JOBS__STEP_MOCK, + FAILURENOTIFIER__NOTIFYFAILURE_FETCH_PREVIOUS_WORKFLOW_RUN__STEP_MOCK, + FAILURENOTIFIER__NOTIFYFAILURE_FETCH_PREVIOUS_WORKFLOW_RUN_JOBS__STEP_MOCK, + FAILURENOTIFIER__NOTIFYFAILURE__PROCESS_EACH_FAILED_JOB__STEP_MOCK, +]; export default { FAILURENOTIFIER__NOTIFYFAILURE__STEP_MOCKS, From 47adcd982e1fa62f405f399a25cb0fd37d6c96e1 Mon Sep 17 00:00:00 2001 From: Rayane Djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:50:41 +0100 Subject: [PATCH 503/580] prettier --- workflow_tests/mocks/failureNotifierMocks.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/workflow_tests/mocks/failureNotifierMocks.ts b/workflow_tests/mocks/failureNotifierMocks.ts index 72a79d9ff17b..ca4009aef952 100644 --- a/workflow_tests/mocks/failureNotifierMocks.ts +++ b/workflow_tests/mocks/failureNotifierMocks.ts @@ -6,7 +6,13 @@ import {createMockStep} from '../utils/utils'; // notifyfailure const FAILURENOTIFIER__NOTIFYFAILURE__FETCH_WORKFLOW_RUN_JOBS__STEP_MOCK = createMockStep('Fetch Workflow Run Jobs', 'Fetch Workflow Run Jobs', 'NOTIFYFAILURE', [], []); const FAILURENOTIFIER__NOTIFYFAILURE_FETCH_PREVIOUS_WORKFLOW_RUN__STEP_MOCK = createMockStep('Fetch Previous Workflow Run', 'Fetch Previous Workflow Run', 'NOTIFYFAILURE', [], []); -const FAILURENOTIFIER__NOTIFYFAILURE_FETCH_PREVIOUS_WORKFLOW_RUN_JOBS__STEP_MOCK = createMockStep('Fetch Previous Workflow Run Jobs', 'Fetch Previous Workflow Run Jobs', 'NOTIFYFAILURE', [], []); +const FAILURENOTIFIER__NOTIFYFAILURE_FETCH_PREVIOUS_WORKFLOW_RUN_JOBS__STEP_MOCK = createMockStep( + 'Fetch Previous Workflow Run Jobs', + 'Fetch Previous Workflow Run Jobs', + 'NOTIFYFAILURE', + [], + [], +); const FAILURENOTIFIER__NOTIFYFAILURE__PROCESS_EACH_FAILED_JOB__STEP_MOCK = createMockStep('Process Each Failed Job', 'Process Each Failed Job', 'NOTIFYFAILURE', [], []); const FAILURENOTIFIER__NOTIFYFAILURE__STEP_MOCKS = [ From ff255476d2995e1f8b24bd065cee12b07bce6ce6 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Sun, 28 Apr 2024 21:02:59 +0200 Subject: [PATCH 504/580] Remove duplicates --- src/CONST.ts | 1 - src/libs/ReportUtils.ts | 16 ---------------- 2 files changed, 17 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 6d62c30f16ce..66b51184852f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1479,7 +1479,6 @@ const CONST = { INVOICE: 'invoice', SUBMIT: 'submit', TRACK: 'track', - INVOICE: 'invoice', }, REQUEST_TYPE: { DISTANCE: 'distance', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 53c258cf4608..3e6f7f0f2618 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -682,13 +682,6 @@ function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { return report?.type === CONST.REPORT.TYPE.INVOICE; } -/** - * Checks if a report is an invoice report. - */ -function isInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { - return report?.type === CONST.REPORT.TYPE.INVOICE; -} - /** * Checks if a report is an Expense report. */ @@ -880,13 +873,6 @@ function isInvoiceRoom(report: OnyxEntry<Report>): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; } -/** - * Whether the provided report is an invoice room chat. - */ -function isInvoiceRoom(report: OnyxEntry<Report>): boolean { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.INVOICE; -} - /** * Whether the provided report belongs to a Control policy and is an expense chat */ @@ -6681,9 +6667,7 @@ export { temporary_getMoneyRequestOptions, buildOptimisticInvoiceReport, buildOptimisticInviteReportAction, - isInvoiceRoom, getInvoiceChatByParticipants, - isInvoiceReport, }; export type { From 6d80ef6e46bd1975406c4127b254df06c5988349 Mon Sep 17 00:00:00 2001 From: Rory Abraham <47436092+roryabraham@users.noreply.github.com> Date: Sun, 28 Apr 2024 12:09:16 -0700 Subject: [PATCH 505/580] Revert "fix-39841: Finetune animation durations" --- src/CONST.ts | 9 +-- .../config.native.ts | 5 ++ src/hooks/useAnimatedHighlightStyle/config.ts | 8 +++ src/hooks/useAnimatedHighlightStyle/index.ts | 69 +++++-------------- src/libs/actions/Policy.ts | 45 ++++++++---- 5 files changed, 63 insertions(+), 73 deletions(-) create mode 100644 src/hooks/useAnimatedHighlightStyle/config.native.ts create mode 100644 src/hooks/useAnimatedHighlightStyle/config.ts diff --git a/src/CONST.ts b/src/CONST.ts index 66b51184852f..879d9d86cdda 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -79,13 +79,8 @@ const CONST = { // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], ANDROID_PACKAGE_NAME, - WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY: 100, - ANIMATED_HIGHLIGHT_ENTRY_DELAY: 50, - ANIMATED_HIGHLIGHT_ENTRY_DURATION: 300, - ANIMATED_HIGHLIGHT_START_DELAY: 10, - ANIMATED_HIGHLIGHT_START_DURATION: 300, - ANIMATED_HIGHLIGHT_END_DELAY: 800, - ANIMATED_HIGHLIGHT_END_DURATION: 2000, + ANIMATED_HIGHLIGHT_DELAY: 500, + ANIMATED_HIGHLIGHT_DURATION: 500, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATION_IN_TIMING: 100, diff --git a/src/hooks/useAnimatedHighlightStyle/config.native.ts b/src/hooks/useAnimatedHighlightStyle/config.native.ts new file mode 100644 index 000000000000..a62d3a33039e --- /dev/null +++ b/src/hooks/useAnimatedHighlightStyle/config.native.ts @@ -0,0 +1,5 @@ +const DELAY_FACTOR = 1.85; + +export default {}; + +export {DELAY_FACTOR}; diff --git a/src/hooks/useAnimatedHighlightStyle/config.ts b/src/hooks/useAnimatedHighlightStyle/config.ts new file mode 100644 index 000000000000..6010c8c33aa7 --- /dev/null +++ b/src/hooks/useAnimatedHighlightStyle/config.ts @@ -0,0 +1,8 @@ +import {isMobile} from '@libs/Browser'; + +// It takes varying amount of time to navigate to a new page on mobile and desktop +// This variable takes that into account +const DELAY_FACTOR = isMobile() ? 1 : 0.2; +export default {}; + +export {DELAY_FACTOR}; diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts index 4f934fee7652..e438bd2473fa 100644 --- a/src/hooks/useAnimatedHighlightStyle/index.ts +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -1,9 +1,9 @@ import React from 'react'; import {InteractionManager} from 'react-native'; import {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; -import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useTheme from '@hooks/useTheme'; import CONST from '@src/CONST'; +import {DELAY_FACTOR} from './config'; type Props = { /** Border radius of the wrapper */ @@ -12,26 +12,14 @@ type Props = { /** Height of the item that is to be faded */ height: number; - /** Delay before the highlighted item enters */ - itemEnterDelay?: number; - - /** Duration in which the item enters */ - itemEnterDuration?: number; - - /** Delay before the item starts to get highlighted */ - highlightStartDelay?: number; - - /** Duration in which the item gets fully highlighted */ - highlightStartDuration?: number; - - /** Delay before the item starts to get un-highlighted */ - highlightEndDelay?: number; - - /** Duration in which the item gets fully un-highlighted */ - highlightEndDuration?: number; - /** Whether the item should be highlighted */ shouldHighlight: boolean; + + /** Duration of the highlight animation */ + highlightDuration?: number; + + /** Delay before the highlight animation starts */ + delay?: number; }; /** @@ -40,60 +28,37 @@ type Props = { export default function useAnimatedHighlightStyle({ borderRadius, shouldHighlight, - itemEnterDelay = CONST.ANIMATED_HIGHLIGHT_ENTRY_DELAY, - itemEnterDuration = CONST.ANIMATED_HIGHLIGHT_ENTRY_DURATION, - highlightStartDelay = CONST.ANIMATED_HIGHLIGHT_START_DELAY, - highlightStartDuration = CONST.ANIMATED_HIGHLIGHT_START_DURATION, - highlightEndDelay = CONST.ANIMATED_HIGHLIGHT_END_DELAY, - highlightEndDuration = CONST.ANIMATED_HIGHLIGHT_END_DURATION, + highlightDuration = CONST.ANIMATED_HIGHLIGHT_DURATION, + delay = CONST.ANIMATED_HIGHLIGHT_DELAY, height, }: Props) { + const actualDelay = delay * DELAY_FACTOR; const repeatableProgress = useSharedValue(0); const nonRepeatableProgress = useSharedValue(shouldHighlight ? 0 : 1); - const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); const theme = useTheme(); const highlightBackgroundStyle = useAnimatedStyle(() => ({ - backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], [theme.appBG, theme.border]), + backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]), opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), borderRadius, })); React.useEffect(() => { - if (!shouldHighlight || !didScreenTransitionEnd) { + if (!shouldHighlight) { return; } InteractionManager.runAfterInteractions(() => { runOnJS(() => { - nonRepeatableProgress.value = withDelay( - itemEnterDelay, - withTiming(1, {duration: itemEnterDuration, easing: Easing.inOut(Easing.ease)}, (finished) => { - if (!finished) { - return; - } - - repeatableProgress.value = withSequence( - withDelay(highlightStartDelay, withTiming(1, {duration: highlightStartDuration, easing: Easing.inOut(Easing.ease)})), - withDelay(highlightEndDelay, withTiming(0, {duration: highlightEndDuration, easing: Easing.inOut(Easing.ease)})), - ); - }), + nonRepeatableProgress.value = withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})); + repeatableProgress.value = withSequence( + withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})), + withDelay(actualDelay, withTiming(0, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})), ); })(); }); - }, [ - didScreenTransitionEnd, - shouldHighlight, - itemEnterDelay, - itemEnterDuration, - highlightStartDelay, - highlightStartDuration, - highlightEndDelay, - highlightEndDuration, - repeatableProgress, - nonRepeatableProgress, - ]); + }, [shouldHighlight, highlightDuration, actualDelay, repeatableProgress, nonRepeatableProgress]); return highlightBackgroundStyle; } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 6d457f150fe6..c9f62a7a75cf 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -71,6 +71,7 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import type {PolicySelector} from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type { InvitedEmailsToAccountIDs, @@ -3832,10 +3833,26 @@ function openPolicyDistanceRatesPage(policyID?: string) { API.read(READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE, params); } -function navigateWhenEnableFeature(policyID: string) { - setTimeout(() => { - Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID)); - }, CONST.WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY); +function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { + const isNarrowLayout = getIsNarrowLayout(); + if (isNarrowLayout) { + setTimeout(() => { + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID)); + }, 1000); + return; + } + + /** + * The app needs to set a navigation action to the microtask queue, it guarantees to execute Onyx.update first, then the navigation action. + * More details - https://github.com/Expensify/App/issues/37785#issuecomment-1989056726. + */ + new Promise<void>((resolve) => { + resolve(); + }).then(() => { + requestAnimationFrame(() => { + Navigation.navigate(featureRoute); + }); + }); } function enablePolicyCategories(policyID: string, enabled: boolean) { @@ -3881,8 +3898,8 @@ function enablePolicyCategories(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, parameters, onyxData); - if (enabled && getIsNarrowLayout()) { - navigateWhenEnableFeature(policyID); + if (enabled) { + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); } } @@ -3973,8 +3990,8 @@ function enablePolicyDistanceRates(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, parameters, onyxData); - if (enabled && getIsNarrowLayout()) { - navigateWhenEnableFeature(policyID); + if (enabled) { + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)); } } @@ -4065,8 +4082,8 @@ function enablePolicyTags(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_TAGS, parameters, onyxData); - if (enabled && getIsNarrowLayout()) { - navigateWhenEnableFeature(policyID); + if (enabled) { + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAGS.getRoute(policyID)); } } @@ -4178,8 +4195,8 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { } API.write(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData); - if (enabled && getIsNarrowLayout()) { - navigateWhenEnableFeature(policyID); + if (enabled) { + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAXES.getRoute(policyID)); } } @@ -4269,8 +4286,8 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, parameters, onyxData); - if (enabled && getIsNarrowLayout()) { - navigateWhenEnableFeature(policyID); + if (enabled) { + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)); } } From b2f932009cbb77f44af3c3da63efafcb66201f90 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Sun, 28 Apr 2024 21:37:26 +0200 Subject: [PATCH 506/580] Fix invoice report name display --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3e6f7f0f2618..62f3bca843f5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -921,7 +921,7 @@ function isPaidGroupPolicyExpenseReport(report: OnyxEntry<Report>): boolean { * Checks if the supplied report is an invoice report in Open state and status. */ function isOpenInvoiceReport(report: OnyxEntry<Report> | EmptyObject): boolean { - return isInvoiceReport(report) && report?.stateNum === CONST.REPORT.STATE_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; + return isInvoiceReport(report) && report?.statusNum === CONST.REPORT.STATUS_NUM.OPEN; } /** @@ -3604,7 +3604,7 @@ function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, re currency, // We don’t translate reportName because the server response is always in English reportName: `${receiverName} owes ${formattedTotal}`, - stateNum: CONST.REPORT.STATE_NUM.OPEN, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS_NUM.OPEN, total, notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, From a6ff12b3746e28b942da9b35d6d481b821c4f7b1 Mon Sep 17 00:00:00 2001 From: Janic Duplessis <janicduplessis@gmail.com> Date: Sun, 28 Apr 2024 17:23:59 -0400 Subject: [PATCH 507/580] Patch onStartReached improvement --- ...zed-lists+0.73.4+002+osr-improvement.patch | 33 +++++++++ ...ative-web+0.19.9+007+osr-improvement.patch | 70 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch create mode 100644 patches/react-native-web+0.19.9+007+osr-improvement.patch diff --git a/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch b/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch new file mode 100644 index 000000000000..dc45a6758d5c --- /dev/null +++ b/patches/@react-native+virtualized-lists+0.73.4+002+osr-improvement.patch @@ -0,0 +1,33 @@ +diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +index e338d90..70a59bf 100644 +--- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js ++++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +@@ -1219,7 +1219,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> { + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef<any> = null; +- _sentStartForContentLength = 0; ++ _sentStartForFirstVisibleItemKey: ?string = null; + _sentEndForContentLength = 0; + _updateCellsToRenderBatcher: Batchinator; + _viewabilityTuples: Array<ViewabilityHelperCallbackTuple> = []; +@@ -1550,16 +1550,16 @@ class VirtualizedList extends StateSafePureComponent<Props, State> { + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && +- this._listMetrics.getContentLength() !== this._sentStartForContentLength ++ this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey + ) { +- this._sentStartForContentLength = this._listMetrics.getContentLength(); ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + if (!isWithinStartThreshold) { +- this._sentStartForContentLength = 0; ++ this._sentStartForFirstVisibleItemKey = null; + } + if (!isWithinEndThreshold) { + this._sentEndForContentLength = 0; diff --git a/patches/react-native-web+0.19.9+007+osr-improvement.patch b/patches/react-native-web+0.19.9+007+osr-improvement.patch new file mode 100644 index 000000000000..074cac3d0e6f --- /dev/null +++ b/patches/react-native-web+0.19.9+007+osr-improvement.patch @@ -0,0 +1,70 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index b05da08..80aea85 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -332,7 +332,7 @@ class VirtualizedList extends StateSafePureComponent { + zoomScale: 1 + }; + this._scrollRef = null; +- this._sentStartForContentLength = 0; ++ this._sentStartForFirstVisibleItemKey = null; + this._sentEndForContentLength = 0; + this._totalCellLength = 0; + this._totalCellsMeasured = 0; +@@ -1397,8 +1397,8 @@ class VirtualizedList extends StateSafePureComponent { + // Next check if the user just scrolled within the start threshold + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed +- else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey) { ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({ + distanceFromStart + }); +@@ -1407,7 +1407,7 @@ class VirtualizedList extends StateSafePureComponent { + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { +- this._sentStartForContentLength = isWithinStartThreshold ? this._sentStartForContentLength : 0; ++ this._sentStartForFirstVisibleItemKey = isWithinStartThreshold ? this._sentStartForFirstVisibleItemKey : null; + this._sentEndForContentLength = isWithinEndThreshold ? this._sentEndForContentLength : 0; + } + } +diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +index 459f017..799a6ee 100644 +--- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +@@ -1325,7 +1325,7 @@ class VirtualizedList extends StateSafePureComponent<Props, State> { + zoomScale: 1, + }; + _scrollRef: ?React.ElementRef<any> = null; +- _sentStartForContentLength = 0; ++ _sentStartForFirstVisibleItemKey: ?string = null; + _sentEndForContentLength = 0; + _totalCellLength = 0; + _totalCellsMeasured = 0; +@@ -1675,18 +1675,18 @@ class VirtualizedList extends StateSafePureComponent<Props, State> { + onStartReached != null && + this.state.cellsAroundViewport.first === 0 && + isWithinStartThreshold && +- this._scrollMetrics.contentLength !== this._sentStartForContentLength ++ this.state.firstVisibleItemKey !== this._sentStartForFirstVisibleItemKey + ) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ this._sentStartForFirstVisibleItemKey = this.state.firstVisibleItemKey; + onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, + // cause onStartReached or onEndReached to be triggered again + else { +- this._sentStartForContentLength = isWithinStartThreshold +- ? this._sentStartForContentLength +- : 0; ++ this._sentStartForFirstVisibleItemKey = isWithinStartThreshold ++ ? this._sentStartForFirstVisibleItemKey ++ : null; + this._sentEndForContentLength = isWithinEndThreshold + ? this._sentEndForContentLength + : 0; From 199c9181eda95e4f4e4c273adcc2e4daa20547b8 Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Mon, 29 Apr 2024 00:20:06 +0200 Subject: [PATCH 508/580] fix remaining wrappers --- .../AdminPolicyAccessOrNotFoundWrapper.tsx | 91 +++++++++++++ .../workspace/WorkspaceMoreFeaturesPage.tsx | 1 - src/pages/workspace/WorkspaceNamePage.tsx | 9 +- .../WorkspaceProfileDescriptionPage.tsx | 9 +- .../workspace/WorkspaceProfileSharePage.tsx | 9 +- ...uickbooksCompanyCardExpenseAccountPage.tsx | 122 +++++++++--------- ...anyCardExpenseAccountPayableSelectPage.tsx | 36 +++--- ...ompanyCardExpenseAccountSelectCardPage.tsx | 48 ++++--- 8 files changed, 208 insertions(+), 117 deletions(-) create mode 100644 src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx diff --git a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx new file mode 100644 index 000000000000..207c277a65d1 --- /dev/null +++ b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx @@ -0,0 +1,91 @@ +/* eslint-disable rulesdir/no-negated-variables */ +import React, {useEffect} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import * as Policy from '@userActions/Policy'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type AdminAccessOrNotFoundOnyxProps = { + /** The report currently being looked at */ + policy: OnyxEntry<OnyxTypes.Policy>; + + /** Indicated whether the report data is loading */ + isLoadingReportData: OnyxEntry<boolean>; +}; + +type AdminPolicyAccessOrNotFoundComponentProps = AdminAccessOrNotFoundOnyxProps & { + /** The children to render */ + children: ((props: AdminAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; + + /** The report currently being looked at */ + policyID: string; + + /** Function to call when pressing the navigation link */ + onLinkPress?: () => void; + + /** The key in the translations file to use for the subtitle */ + subtitleKey?: TranslationPaths; +}; + +function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFoundComponentProps) { + const isPolicyIDInRoute = !!props.policyID?.length; + + useEffect(() => { + if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { + // If the workspace is not required or is already loaded, we don't need to call the API + return; + } + + Policy.openWorkspace(props.policyID, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPolicyIDInRoute, props.policyID]); + + const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + + const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPendingDeletePolicy(props.policy); + + if (shouldShowFullScreenLoadingIndicator) { + return <FullscreenLoadingIndicator />; + } + + if (shouldShowNotFoundPage) { + const isPolicyNotAccessible = isEmptyObject(props.policy) || !props.policy?.id; + + if (isPolicyNotAccessible) { + return ( + <ScreenWrapper testID={AdminPolicyAccessOrNotFoundComponent.displayName}> + <FullPageNotFoundView + shouldShow + shouldForceFullScreen + onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} + onLinkPress={props.onLinkPress} + subtitleKey={props.subtitleKey} + /> + </ScreenWrapper> + ); + } + return <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; + } + + return typeof props.children === 'function' ? props.children(props) : props.children; +} +AdminPolicyAccessOrNotFoundComponent.displayName = 'AdminPolicyAccessOrNotFoundComponent'; + +export default withOnyx<AdminPolicyAccessOrNotFoundComponentProps, AdminAccessOrNotFoundOnyxProps>({ + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, + }, + isLoadingReportData: { + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + }, +})(AdminPolicyAccessOrNotFoundComponent); diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index cb258dc85016..457e9e805e96 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -1,6 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import {CONST} from 'expensify-common/lib/CONST'; import React, {useCallback} from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; diff --git a/src/pages/workspace/WorkspaceNamePage.tsx b/src/pages/workspace/WorkspaceNamePage.tsx index 96c045332271..3a0cd476de9b 100644 --- a/src/pages/workspace/WorkspaceNamePage.tsx +++ b/src/pages/workspace/WorkspaceNamePage.tsx @@ -15,7 +15,7 @@ import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WorkspaceSettingsForm'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import withPolicy from './withPolicy'; import type {WithPolicyProps} from './withPolicy'; @@ -54,7 +54,10 @@ function WorkspaceNamePage({policy}: Props) { }, []); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''}> + <AccessOrNotFoundWrapper + policyID={policy?.id ?? ''} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + > <ScreenWrapper includeSafeAreaPaddingBottom={false} shouldEnableMaxHeight @@ -88,7 +91,7 @@ function WorkspaceNamePage({policy}: Props) { </View> </FormProvider> </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx index 865406326e2e..00b11bf64697 100644 --- a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx +++ b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx @@ -17,7 +17,7 @@ import variables from '@styles/variables'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import withPolicy from './withPolicy'; import type {WithPolicyProps} from './withPolicy'; @@ -69,7 +69,10 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policy?.id ?? ''}> + <AccessOrNotFoundWrapper + policyID={policy?.id ?? ''} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + > <ScreenWrapper includeSafeAreaPaddingBottom={false} shouldEnableMaxHeight @@ -111,7 +114,7 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { </View> </FormProvider> </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx index 5aab39f8e019..7e3a21a7f88d 100644 --- a/src/pages/workspace/WorkspaceProfileSharePage.tsx +++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx @@ -19,7 +19,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as Url from '@libs/Url'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import withPolicy from './withPolicy'; import type {WithPolicyProps} from './withPolicy'; @@ -39,7 +39,10 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) { const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_JOIN_USER.getRoute(id, adminEmail)}`; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={id}> + <AccessOrNotFoundWrapper + policyID={id} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + > <ScreenWrapper testID={WorkspaceProfileSharePage.displayName} shouldShowOfflineIndicatorInWideScreen @@ -83,7 +86,7 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) { </View> </ScrollView> </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx index ee1497ed3e60..498236d0912d 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx @@ -9,8 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import Navigation from '@navigation/Navigation'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; @@ -24,70 +23,67 @@ function QuickbooksCompanyCardExpenseAccountPage({policy}: WithPolicyConnections const {exportCompanyCardAccount, exportAccountPayable, autoCreateVendor, errorFields, pendingFields, exportCompanyCard} = policy?.connections?.quickbooksOnline?.config ?? {}; const isVendorSelected = exportCompanyCard === CONST.QUICKBOOKS_EXPORT_COMPANY_CARD.VENDOR_BILL; return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + <AccessOrNotFoundWrapper + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + > + <ScreenWrapper + includeSafeAreaPaddingBottom={false} + testID={QuickbooksCompanyCardExpenseAccountPage.displayName} > - <ScreenWrapper - includeSafeAreaPaddingBottom={false} - testID={QuickbooksCompanyCardExpenseAccountPage.displayName} - > - <HeaderWithBackButton title={translate('workspace.qbo.exportCompany')} /> - <ScrollView contentContainerStyle={styles.pb2}> - <Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.exportCompanyCardsDescription')}</Text> - <OfflineWithFeedback pendingAction={pendingFields?.exportCompanyCard}> - <MenuItemWithTopDescription - title={exportCompanyCard ? translate(`workspace.qbo.${exportCompanyCard}`) : undefined} - description={translate('workspace.qbo.exportCompany')} - error={errorFields?.exportCompanyCard ? translate('common.genericErrorMessage') : undefined} - onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_SELECT.getRoute(policyID))} - brickRoadIndicator={errorFields?.exportCompanyCard ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - shouldShowRightIcon - /> - </OfflineWithFeedback> - {!!exportCompanyCard && ( - <Text style={[styles.ph5, styles.mutedNormalTextLabel, styles.pt1, styles.pb2]}>{translate(`workspace.qbo.${exportCompanyCard}Description`)}</Text> - )} - {isVendorSelected && ( - <> - <OfflineWithFeedback pendingAction={pendingFields?.exportAccountPayable}> - <MenuItemWithTopDescription - title={exportAccountPayable} - description={translate('workspace.qbo.accountsPayable')} - error={errorFields?.exportAccountPayable ? translate('common.genericErrorMessage') : undefined} - onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT.getRoute(policyID))} - brickRoadIndicator={errorFields?.exportAccountPayable ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - shouldShowRightIcon - /> - </OfflineWithFeedback> - <ToggleSettingOptionRow - subtitle={translate('workspace.qbo.defaultVendorDescription')} - errors={errorFields?.autoCreateVendor ?? undefined} - title={translate('workspace.qbo.defaultVendor')} - wrapperStyle={[styles.ph5, styles.mb3, styles.mt1]} - isActive={Boolean(autoCreateVendor)} - onToggle={(isOn) => - Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.QUICK_BOOKS_CONFIG.AUTO_CREATE_VENDOR, isOn) - } - pendingAction={pendingFields?.autoCreateVendor} + <HeaderWithBackButton title={translate('workspace.qbo.exportCompany')} /> + <ScrollView contentContainerStyle={styles.pb2}> + <Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.exportCompanyCardsDescription')}</Text> + <OfflineWithFeedback pendingAction={pendingFields?.exportCompanyCard}> + <MenuItemWithTopDescription + title={exportCompanyCard ? translate(`workspace.qbo.${exportCompanyCard}`) : undefined} + description={translate('workspace.qbo.exportCompany')} + error={errorFields?.exportCompanyCard ? translate('common.genericErrorMessage') : undefined} + onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_SELECT.getRoute(policyID))} + brickRoadIndicator={errorFields?.exportCompanyCard ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + shouldShowRightIcon + /> + </OfflineWithFeedback> + {!!exportCompanyCard && ( + <Text style={[styles.ph5, styles.mutedNormalTextLabel, styles.pt1, styles.pb2]}>{translate(`workspace.qbo.${exportCompanyCard}Description`)}</Text> + )} + {isVendorSelected && ( + <> + <OfflineWithFeedback pendingAction={pendingFields?.exportAccountPayable}> + <MenuItemWithTopDescription + title={exportAccountPayable} + description={translate('workspace.qbo.accountsPayable')} + error={errorFields?.exportAccountPayable ? translate('common.genericErrorMessage') : undefined} + onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_PAYABLE_SELECT.getRoute(policyID))} + brickRoadIndicator={errorFields?.exportAccountPayable ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + shouldShowRightIcon /> - </> - )} - <OfflineWithFeedback pendingAction={pendingFields?.exportCompanyCardAccount}> - <MenuItemWithTopDescription - title={exportCompanyCardAccount} - description={isVendorSelected ? translate('workspace.qbo.vendor') : translate('workspace.qbo.account')} - onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT.getRoute(policyID))} - brickRoadIndicator={errorFields?.exportCompanyCardAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - shouldShowRightIcon - error={errorFields?.exportCompanyCardAccount ? translate('common.genericErrorMessage') : undefined} + </OfflineWithFeedback> + <ToggleSettingOptionRow + subtitle={translate('workspace.qbo.defaultVendorDescription')} + errors={errorFields?.autoCreateVendor ?? undefined} + title={translate('workspace.qbo.defaultVendor')} + wrapperStyle={[styles.ph5, styles.mb3, styles.mt1]} + isActive={Boolean(autoCreateVendor)} + onToggle={(isOn) => Connections.updatePolicyConnectionConfig(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.QUICK_BOOKS_CONFIG.AUTO_CREATE_VENDOR, isOn)} + pendingAction={pendingFields?.autoCreateVendor} /> - </OfflineWithFeedback> - </ScrollView> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </> + )} + <OfflineWithFeedback pendingAction={pendingFields?.exportCompanyCardAccount}> + <MenuItemWithTopDescription + title={exportCompanyCardAccount} + description={isVendorSelected ? translate('workspace.qbo.vendor') : translate('workspace.qbo.account')} + onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_COMPANY_CARD_EXPENSE_ACCOUNT_SELECT.getRoute(policyID))} + brickRoadIndicator={errorFields?.exportCompanyCardAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + shouldShowRightIcon + error={errorFields?.exportCompanyCardAccount ? translate('common.genericErrorMessage') : undefined} + /> + </OfflineWithFeedback> + </ScrollView> + </ScreenWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage.tsx index 6e363d998039..f9aef6e2e85a 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPayableSelectPage.tsx @@ -9,8 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import Navigation from '@navigation/Navigation'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; @@ -49,23 +48,22 @@ function QuickbooksCompanyCardExpenseAccountPayableSelectPage({policy}: WithPoli ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} - > - <ScreenWrapper testID={QuickbooksCompanyCardExpenseAccountPayableSelectPage.displayName}> - <HeaderWithBackButton title={translate('workspace.qbo.accountsPayable')} /> - <SelectionList - headerContent={<Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.accountsPayableDescription')}</Text>} - sections={[{data}]} - ListItem={RadioListItem} - onSelectRow={selectAccountPayable} - initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} - /> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <AccessOrNotFoundWrapper + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + > + <ScreenWrapper testID={QuickbooksCompanyCardExpenseAccountPayableSelectPage.displayName}> + <HeaderWithBackButton title={translate('workspace.qbo.accountsPayable')} /> + <SelectionList + headerContent={<Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.accountsPayableDescription')}</Text>} + sections={[{data}]} + ListItem={RadioListItem} + onSelectRow={selectAccountPayable} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + /> + </ScreenWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx index 00a82ddb424b..e67922e6a773 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountSelectCardPage.tsx @@ -12,8 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections'; import Navigation from '@navigation/Navigation'; -import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import CONST from '@src/CONST'; @@ -76,29 +75,28 @@ function QuickbooksCompanyCardExpenseAccountSelectCardPage({policy}: WithPolicyC ); return ( - <AdminPolicyAccessOrNotFoundWrapper policyID={policyID}> - <FeatureEnabledAccessOrNotFoundWrapper - policyID={policyID} - featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} - > - <ScreenWrapper testID={QuickbooksCompanyCardExpenseAccountSelectCardPage.displayName}> - <HeaderWithBackButton title={translate('workspace.qbo.exportCompany')} /> - <View style={styles.flex1}> - <SelectionList - containerStyle={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto]} - headerContent={<Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.exportCompanyCardsDescription')}</Text>} - sections={sections} - ListItem={RadioListItem} - onSelectRow={selectExportCompanyCard} - initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} - footerContent={ - isLocationEnabled && <Text style={[styles.mutedNormalTextLabel, styles.pt2]}>{translate('workspace.qbo.companyCardsLocationEnabledDescription')}</Text> - } - /> - </View> - </ScreenWrapper> - </FeatureEnabledAccessOrNotFoundWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + <AccessOrNotFoundWrapper + policyID={policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} + featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} + > + <ScreenWrapper testID={QuickbooksCompanyCardExpenseAccountSelectCardPage.displayName}> + <HeaderWithBackButton title={translate('workspace.qbo.exportCompany')} /> + <View style={styles.flex1}> + <SelectionList + containerStyle={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto]} + headerContent={<Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.exportCompanyCardsDescription')}</Text>} + sections={sections} + ListItem={RadioListItem} + onSelectRow={selectExportCompanyCard} + initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList} + footerContent={ + isLocationEnabled && <Text style={[styles.mutedNormalTextLabel, styles.pt2]}>{translate('workspace.qbo.companyCardsLocationEnabledDescription')}</Text> + } + /> + </View> + </ScreenWrapper> + </AccessOrNotFoundWrapper> ); } From dd27831245d29f39425f31f88ae95f4f1e08f25f Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Mon, 29 Apr 2024 00:30:29 +0200 Subject: [PATCH 509/580] remove admin wrapper --- .../BlockingViews/FullPageNotFoundView.tsx | 1 + src/pages/ErrorPage/NotFoundPage.tsx | 7 +- .../workspace/AccessOrNotFoundWrapper.tsx | 19 +++- .../AdminPolicyAccessOrNotFoundWrapper.tsx | 91 ------------------- .../workspace/WorkspaceInviteMessagePage.tsx | 7 +- src/pages/workspace/WorkspaceInvitePage.tsx | 7 +- .../WorkspaceProfileCurrencyPage.tsx | 8 +- 7 files changed, 33 insertions(+), 107 deletions(-) delete mode 100644 src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 5039de3b20b6..e1cd5c94d4c9 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -86,4 +86,5 @@ function FullPageNotFoundView({ FullPageNotFoundView.displayName = 'FullPageNotFoundView'; +export type {FullPageNotFoundViewProps}; export default FullPageNotFoundView; diff --git a/src/pages/ErrorPage/NotFoundPage.tsx b/src/pages/ErrorPage/NotFoundPage.tsx index a324b048119a..7d5dd180709a 100644 --- a/src/pages/ErrorPage/NotFoundPage.tsx +++ b/src/pages/ErrorPage/NotFoundPage.tsx @@ -1,18 +1,21 @@ import React from 'react'; +import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ScreenWrapper from '@components/ScreenWrapper'; type NotFoundPageProps = { onBackButtonPress?: () => void; -}; +} & Pick<FullPageNotFoundViewProps, 'subtitleKey' | 'onLinkPress'>; // eslint-disable-next-line rulesdir/no-negated-variables -function NotFoundPage({onBackButtonPress}: NotFoundPageProps) { +function NotFoundPage({onBackButtonPress, subtitleKey, onLinkPress}: NotFoundPageProps) { return ( <ScreenWrapper testID={NotFoundPage.displayName}> <FullPageNotFoundView shouldShow onBackButtonPress={onBackButtonPress} + subtitleKey={subtitleKey} + onLinkPress={onLinkPress} /> </ScreenWrapper> ); diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index e7477b3c05d4..c352717f5499 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -2,6 +2,7 @@ import React, {useEffect} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; @@ -42,23 +43,29 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { /** The current feature name that the user tries to get access to */ featureName?: PolicyFeatureName; -}; +} & Pick<FullPageNotFoundViewProps, 'subtitleKey' | 'onLinkPress'>; -type PageNotFoundFallbackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID'> & {shouldShowFullScreenFallback: boolean}; +type PageNotFoundFallbackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID' | 'subtitleKey' | 'onLinkPress'> & {shouldShowFullScreenFallback: boolean}; -function PageNotFoundFallback({policyID, shouldShowFullScreenFallback}: PageNotFoundFallbackProps) { +function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, subtitleKey, onLinkPress}: PageNotFoundFallbackProps) { return shouldShowFullScreenFallback ? ( <FullPageNotFoundView shouldShow onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} shouldForceFullScreen + subtitleKey={subtitleKey} + onLinkPress={onLinkPress} /> ) : ( - <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(policyID))} /> + <NotFoundPage + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(policyID))} + subtitleKey={subtitleKey} + onLinkPress={onLinkPress} + /> ); } -function AccessOrNotFoundWrapper({accessVariants = [], ...props}: AccessOrNotFoundWrapperProps) { +function AccessOrNotFoundWrapper({accessVariants = [], subtitleKey, onLinkPress, ...props}: AccessOrNotFoundWrapperProps) { const {policy, policyID, featureName, isLoadingReportData} = props; const isPolicyIDInRoute = !!policyID?.length; @@ -93,6 +100,8 @@ function AccessOrNotFoundWrapper({accessVariants = [], ...props}: AccessOrNotFou <PageNotFoundFallback policyID={policyID} shouldShowFullScreenFallback={!isFeatureEnabled} + subtitleKey={subtitleKey} + onLinkPress={onLinkPress} /> ); } diff --git a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx b/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx deleted file mode 100644 index 207c277a65d1..000000000000 --- a/src/pages/workspace/AdminPolicyAccessOrNotFoundWrapper.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint-disable rulesdir/no-negated-variables */ -import React, {useEffect} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import ScreenWrapper from '@components/ScreenWrapper'; -import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import * as Policy from '@userActions/Policy'; -import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; - -type AdminAccessOrNotFoundOnyxProps = { - /** The report currently being looked at */ - policy: OnyxEntry<OnyxTypes.Policy>; - - /** Indicated whether the report data is loading */ - isLoadingReportData: OnyxEntry<boolean>; -}; - -type AdminPolicyAccessOrNotFoundComponentProps = AdminAccessOrNotFoundOnyxProps & { - /** The children to render */ - children: ((props: AdminAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; - - /** The report currently being looked at */ - policyID: string; - - /** Function to call when pressing the navigation link */ - onLinkPress?: () => void; - - /** The key in the translations file to use for the subtitle */ - subtitleKey?: TranslationPaths; -}; - -function AdminPolicyAccessOrNotFoundComponent(props: AdminPolicyAccessOrNotFoundComponentProps) { - const isPolicyIDInRoute = !!props.policyID?.length; - - useEffect(() => { - if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { - // If the workspace is not required or is already loaded, we don't need to call the API - return; - } - - Policy.openWorkspace(props.policyID, []); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPolicyIDInRoute, props.policyID]); - - const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); - - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyAdmin(props.policy) || PolicyUtils.isPendingDeletePolicy(props.policy); - - if (shouldShowFullScreenLoadingIndicator) { - return <FullscreenLoadingIndicator />; - } - - if (shouldShowNotFoundPage) { - const isPolicyNotAccessible = isEmptyObject(props.policy) || !props.policy?.id; - - if (isPolicyNotAccessible) { - return ( - <ScreenWrapper testID={AdminPolicyAccessOrNotFoundComponent.displayName}> - <FullPageNotFoundView - shouldShow - shouldForceFullScreen - onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} - onLinkPress={props.onLinkPress} - subtitleKey={props.subtitleKey} - /> - </ScreenWrapper> - ); - } - return <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; - } - - return typeof props.children === 'function' ? props.children(props) : props.children; -} -AdminPolicyAccessOrNotFoundComponent.displayName = 'AdminPolicyAccessOrNotFoundComponent'; - -export default withOnyx<AdminPolicyAccessOrNotFoundComponentProps, AdminAccessOrNotFoundOnyxProps>({ - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, - }, - isLoadingReportData: { - key: ONYXKEYS.IS_LOADING_REPORT_DATA, - }, -})(AdminPolicyAccessOrNotFoundComponent); diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 84ee1e1bf70f..801d7be264cd 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -36,7 +36,7 @@ import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceInviteMessageForm'; import type {InvitedEmailsToAccountIDs, PersonalDetailsList} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import SearchInputManager from './SearchInputManager'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -133,8 +133,9 @@ function WorkspaceInviteMessagePage({ const policyName = policy?.name; return ( - <AdminPolicyAccessOrNotFoundWrapper + <AccessOrNotFoundWrapper policyID={route.params.policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} onLinkPress={PolicyUtils.goBackFromInvalidPolicy} > @@ -216,7 +217,7 @@ function WorkspaceInviteMessagePage({ </View> </FormProvider> </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 08799d4bf5ac..052b8e77c5b0 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -32,7 +32,7 @@ import type SCREENS from '@src/SCREENS'; import type {Beta, InvitedEmailsToAccountIDs} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import SearchInputManager from './SearchInputManager'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -280,8 +280,9 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli ); return ( - <AdminPolicyAccessOrNotFoundWrapper + <AccessOrNotFoundWrapper policyID={route.params.policyID} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} onLinkPress={PolicyUtils.goBackFromInvalidPolicy} > @@ -321,7 +322,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli footerContent={footerContent} /> </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index 20ffe410a196..a62b69fb201e 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -9,10 +9,11 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CurrencyList} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWrapper'; +import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -71,8 +72,9 @@ function WorkspaceProfileCurrencyPage({currencyList = {}, policy}: WorkspaceProf }; return ( - <AdminPolicyAccessOrNotFoundWrapper + <AccessOrNotFoundWrapper policyID={policy?.id ?? ''} + accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} onLinkPress={PolicyUtils.goBackFromInvalidPolicy} subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} > @@ -97,7 +99,7 @@ function WorkspaceProfileCurrencyPage({currencyList = {}, policy}: WorkspaceProf showScrollIndicator /> </ScreenWrapper> - </AdminPolicyAccessOrNotFoundWrapper> + </AccessOrNotFoundWrapper> ); } From c36bd3bec9a56c42b628ed7d188cbfd85061c4f6 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 29 Apr 2024 08:10:19 +0200 Subject: [PATCH 510/580] Hide payment button for invoices --- src/components/MoneyReportHeader.tsx | 2 +- src/components/ReportActionItem/ReportPreview.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 522158b8edb7..7e0139b147fd 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -109,7 +109,7 @@ function MoneyReportHeader({ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); - const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(moneyRequestReport) && (shouldShowPayButton || shouldShowApproveButton); const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 5c78e1e2604e..57563d1ac181 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -210,7 +210,7 @@ function ReportPreview({ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); - const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(iouReport) && (shouldShowPayButton || shouldShowApproveButton); const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID); const shouldShowRBR = !iouSettled && hasErrors; From 88db1535e423a8b78666c5cbfedad57b4fd598fa Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 29 Apr 2024 08:35:12 +0200 Subject: [PATCH 511/580] Disable invoice editing for the invoice receiver --- src/libs/ReportUtils.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 62f3bca843f5..0bac3e7ce47e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2534,6 +2534,10 @@ function canEditMoneyRequest(reportAction: OnyxEntry<ReportAction>): boolean { const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; const isManager = currentUserAccountID === moneyRequestReport?.managerID; + if (isInvoiceReport(moneyRequestReport) && isManager) { + return false; + } + // Admin & managers can always edit coding fields such as tag, category, billable, etc. As long as the report has a state higher than OPEN. if ((isAdmin || isManager) && !isOpenExpenseReport(moneyRequestReport)) { return true; From 00f490a97cba7d6537f18eafb2b5fc0ea72d082f Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 29 Apr 2024 08:52:42 +0200 Subject: [PATCH 512/580] Disable invoice receipt update --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0bac3e7ce47e..9fa4d327581f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2594,7 +2594,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry<ReportAction>, field if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { const isRequestor = currentUserAccountID === reportAction?.actorAccountID; - return !TransactionUtils.isReceiptBeingScanned(transaction) && !TransactionUtils.isDistanceRequest(transaction) && isRequestor; + return !isInvoiceReport(moneyRequestReport) && !TransactionUtils.isReceiptBeingScanned(transaction) && !TransactionUtils.isDistanceRequest(transaction) && isRequestor; } return true; From 603f752247ecf2d5dbffd38f73b91024314c1b68 Mon Sep 17 00:00:00 2001 From: dhairyasenjaliya <dhairyasenjaliya@gmail.com> Date: Mon, 29 Apr 2024 13:10:47 +0530 Subject: [PATCH 513/580] Added overflowhidden to default image preview component used on ReceiptImage --- src/components/ReceiptImage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReceiptImage.tsx b/src/components/ReceiptImage.tsx index 118fe769e52b..3249d688eed7 100644 --- a/src/components/ReceiptImage.tsx +++ b/src/components/ReceiptImage.tsx @@ -128,7 +128,7 @@ function ReceiptImage({ return ( <Image source={{uri: source}} - style={style ?? [styles.w100, styles.h100]} + style={[style ?? [styles.w100, styles.h100], styles.overflowHidden]} isAuthTokenRequired={isAuthTokenRequired} /> ); From dcad06e498f5d1829f9d29bab1baf7752ecd352d Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Mon, 29 Apr 2024 09:53:22 +0200 Subject: [PATCH 514/580] Refactor SearchUtils --- src/libs/SearchUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index 77185ccbac02..f524fd5b6cc1 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,4 +1,4 @@ -import TransactionListItem from '@components/SelectionList/TemporaryTransactionListItem'; +import TransactionListItem from '@components/SelectionList/TransactionListItem'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type {SearchTransaction} from '@src/types/onyx/SearchResults'; From 8fc14266108caf16386160a52d082cbd98bc0c56 Mon Sep 17 00:00:00 2001 From: BrtqKr <bartlomiej.krason@swmansion.com> Date: Mon, 29 Apr 2024 10:48:38 +0200 Subject: [PATCH 515/580] add props spread for not found page --- src/pages/ErrorPage/NotFoundPage.tsx | 8 ++++---- .../workspace/AccessOrNotFoundWrapper.tsx | 20 ++++++++++--------- .../workspace/WorkspaceInviteMessagePage.tsx | 3 +-- src/pages/workspace/WorkspaceInvitePage.tsx | 3 +-- .../WorkspaceProfileCurrencyPage.tsx | 3 +-- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/pages/ErrorPage/NotFoundPage.tsx b/src/pages/ErrorPage/NotFoundPage.tsx index 7d5dd180709a..cfdeab9c51c7 100644 --- a/src/pages/ErrorPage/NotFoundPage.tsx +++ b/src/pages/ErrorPage/NotFoundPage.tsx @@ -5,17 +5,17 @@ import ScreenWrapper from '@components/ScreenWrapper'; type NotFoundPageProps = { onBackButtonPress?: () => void; -} & Pick<FullPageNotFoundViewProps, 'subtitleKey' | 'onLinkPress'>; +} & FullPageNotFoundViewProps; // eslint-disable-next-line rulesdir/no-negated-variables -function NotFoundPage({onBackButtonPress, subtitleKey, onLinkPress}: NotFoundPageProps) { +function NotFoundPage({onBackButtonPress, ...fullPageNotFoundViewProps}: NotFoundPageProps) { return ( <ScreenWrapper testID={NotFoundPage.displayName}> <FullPageNotFoundView shouldShow onBackButtonPress={onBackButtonPress} - subtitleKey={subtitleKey} - onLinkPress={onLinkPress} + // eslint-disable-next-line react/jsx-props-no-spreading + {...fullPageNotFoundViewProps} /> </ScreenWrapper> ); diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index c352717f5499..b6b641979319 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -43,29 +43,32 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & { /** The current feature name that the user tries to get access to */ featureName?: PolicyFeatureName; + + /** Props for customizing fallback pages */ + fullPageNotFoundViewProps?: FullPageNotFoundViewProps; } & Pick<FullPageNotFoundViewProps, 'subtitleKey' | 'onLinkPress'>; -type PageNotFoundFallbackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID' | 'subtitleKey' | 'onLinkPress'> & {shouldShowFullScreenFallback: boolean}; +type PageNotFoundFallbackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID' | 'fullPageNotFoundViewProps'> & {shouldShowFullScreenFallback: boolean}; -function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, subtitleKey, onLinkPress}: PageNotFoundFallbackProps) { +function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageNotFoundViewProps}: PageNotFoundFallbackProps) { return shouldShowFullScreenFallback ? ( <FullPageNotFoundView shouldShow onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} shouldForceFullScreen - subtitleKey={subtitleKey} - onLinkPress={onLinkPress} + // eslint-disable-next-line react/jsx-props-no-spreading + {...fullPageNotFoundViewProps} /> ) : ( <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(policyID))} - subtitleKey={subtitleKey} - onLinkPress={onLinkPress} + // eslint-disable-next-line react/jsx-props-no-spreading + {...fullPageNotFoundViewProps} /> ); } -function AccessOrNotFoundWrapper({accessVariants = [], subtitleKey, onLinkPress, ...props}: AccessOrNotFoundWrapperProps) { +function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps, ...props}: AccessOrNotFoundWrapperProps) { const {policy, policyID, featureName, isLoadingReportData} = props; const isPolicyIDInRoute = !!policyID?.length; @@ -100,8 +103,7 @@ function AccessOrNotFoundWrapper({accessVariants = [], subtitleKey, onLinkPress, <PageNotFoundFallback policyID={policyID} shouldShowFullScreenFallback={!isFeatureEnabled} - subtitleKey={subtitleKey} - onLinkPress={onLinkPress} + fullPageNotFoundViewProps={fullPageNotFoundViewProps} /> ); } diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 801d7be264cd..2eaa38b865e6 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -136,8 +136,7 @@ function WorkspaceInviteMessagePage({ <AccessOrNotFoundWrapper policyID={route.params.policyID} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + fullPageNotFoundViewProps={{subtitleKey: isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized', onLinkPress: PolicyUtils.goBackFromInvalidPolicy}} > <ScreenWrapper includeSafeAreaPaddingBottom={false} diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 052b8e77c5b0..abb7b0a15944 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -283,8 +283,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli <AccessOrNotFoundWrapper policyID={route.params.policyID} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} + fullPageNotFoundViewProps={{subtitleKey: isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized', onLinkPress: PolicyUtils.goBackFromInvalidPolicy}} > <ScreenWrapper shouldEnableMaxHeight diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index a62b69fb201e..bef697f37a88 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -75,8 +75,7 @@ function WorkspaceProfileCurrencyPage({currencyList = {}, policy}: WorkspaceProf <AccessOrNotFoundWrapper policyID={policy?.id ?? ''} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} - onLinkPress={PolicyUtils.goBackFromInvalidPolicy} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + fullPageNotFoundViewProps={{onLinkPress: PolicyUtils.goBackFromInvalidPolicy, subtitleKey: isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'}} > <ScreenWrapper includeSafeAreaPaddingBottom={false} From cb13e9c8e707e0a8218cca26067c82f9a2776d75 Mon Sep 17 00:00:00 2001 From: Agata Kosior <agata.kosior57@gmail.com> Date: Mon, 29 Apr 2024 11:39:49 +0200 Subject: [PATCH 516/580] fix: comment change --- src/hooks/useReimbursementAccountStepFormSubmit.ts | 2 +- src/hooks/useStepFormSubmit.ts | 2 +- src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useReimbursementAccountStepFormSubmit.ts b/src/hooks/useReimbursementAccountStepFormSubmit.ts index d0d9dd07035a..c18e2e33ed04 100644 --- a/src/hooks/useReimbursementAccountStepFormSubmit.ts +++ b/src/hooks/useReimbursementAccountStepFormSubmit.ts @@ -12,7 +12,7 @@ type UseReimbursementAccountStepFormSubmitParams = Pick<SubStepProps, 'onNext'> /** * Hook for handling submit method in ReimbursementAccount substeps. - * When user is in editing mode we should save values only when user confirm that + * When user is in editing mode, we should save values only when user confirms the change * @param onNext - callback * @param fieldIds - field IDs for particular step * @param shouldSaveDraft - if we should save draft values diff --git a/src/hooks/useStepFormSubmit.ts b/src/hooks/useStepFormSubmit.ts index 86d754bf2fc9..0a0503516127 100644 --- a/src/hooks/useStepFormSubmit.ts +++ b/src/hooks/useStepFormSubmit.ts @@ -12,7 +12,7 @@ type UseStepFormSubmitParams<T extends keyof OnyxFormValuesMapping> = Pick<SubSt /** * Hook for handling submit method in substeps. - * When user is in editing mode we should save values only when user confirm that + * When user is in editing mode, we should save values only when user confirms the change * @param formId - ID for particular form * @param onNext - callback * @param fieldIds - field IDs for particular step diff --git a/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts index 8674092383a2..48ad9b8cdb21 100644 --- a/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts +++ b/src/hooks/useWalletAdditionalDetailsStepFormSubmit.ts @@ -12,7 +12,7 @@ type UseWalletAdditionalDetailsStepFormSubmitParams = Pick<SubStepProps, 'onNext /** * Hook for handling submit method in WalletAdditionalDetails substeps. - * When user is in editing mode we should save values only when user confirm that + * When user is in editing mode, we should save values only when user confirms the change * @param onNext - callback * @param fieldIds - field IDs for particular step * @param shouldSaveDraft - if we should save draft values From ef766adefdacdc76f138a3423fbf2f56865fd193 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 29 Apr 2024 11:39:57 +0200 Subject: [PATCH 517/580] Fix LHN option display --- src/components/LHNOptionsList/OptionRowLHN.tsx | 1 + src/libs/ReportUtils.ts | 2 ++ src/libs/SidebarUtils.ts | 1 + 3 files changed, 4 insertions(+) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 9946dea9d5a7..850173433cf0 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -202,6 +202,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti !!optionItem.isTaskReport || !!optionItem.isThread || !!optionItem.isMoneyRequestReport || + !!optionItem.isInvoiceReport || ReportUtils.isGroupChat(report) } /> diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9fa4d327581f..6e8bc51c8848 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -426,6 +426,7 @@ type OptionData = { shouldShowSubscript?: boolean | null; isPolicyExpenseChat?: boolean | null; isMoneyRequestReport?: boolean | null; + isInvoiceReport?: boolean; isExpenseRequest?: boolean | null; isAllowedToComment?: boolean | null; isThread?: boolean | null; @@ -5906,6 +5907,7 @@ function isDeprecatedGroupDM(report: OnyxEntry<Report>): boolean { report && !isChatThread(report) && !isTaskReport(report) && + !isInvoiceReport(report) && !isMoneyRequestReport(report) && !isArchivedRoom(report) && !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 4f1a35ee1d87..1fa7844fe6b1 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -250,6 +250,7 @@ function getOptionData({ result.isThread = ReportUtils.isChatThread(report); result.isChatRoom = ReportUtils.isChatRoom(report); result.isTaskReport = ReportUtils.isTaskReport(report); + result.isInvoiceReport = ReportUtils.isInvoiceReport(report); result.parentReportAction = parentReportAction; result.isArchivedRoom = ReportUtils.isArchivedRoom(report); result.isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); From c2c2203c3305310f610e6c6ab64fb4736de7ea63 Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Mon, 29 Apr 2024 12:56:51 +0200 Subject: [PATCH 518/580] add styles for narrow screens for TransactionListItem --- src/components/Search.tsx | 101 ++++++++++++++++-- .../SelectionList/TransactionListItem.tsx | 95 +++++++++++++--- src/pages/Search/SearchPage.tsx | 91 +--------------- 3 files changed, 181 insertions(+), 106 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 0fb0db1924a2..1e34eb62bebe 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -1,13 +1,63 @@ import React, {useEffect} from 'react'; +import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as SearchActions from '@libs/actions/Search'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as SearchUtils from '@libs/SearchUtils'; import EmptySearchView from '@pages/Search/EmptySearchView'; +import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import * as Illustrations from './Icon/Illustrations'; +import ScreenWrapper from './ScreenWrapper'; +import SelectionList from './SelectionList'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; +import Text from './Text'; + +const mockData = [ + { + receipt: {source: 'http...'}, + hasEReceipt: false, + created: '2024-04-11 00:00:00', + amount: 12500, + type: 'cash', + reportID: '1', + transactionThreadReportID: '2', + transactionID: '1234', + modifiedCreated: '2024-05-06 00:00:00', + description: 'description description description description', + accountID: '8392101', + managerID: '8392101', + currency: 'USD', + modifiedCurrency: '', + category: 'Bananas', + tag: 'Green', + }, + { + receipt: {source: 'http...'}, + hasEReceipt: false, + created: '2024-04-11 00:00:00', + amount: 12500, + type: 'cash', // not present in live data (data outside of snapshot_) + reportID: '1', + transactionThreadReportID: '2', + transactionID: '5555', + modifiedCreated: '2024-05-06 00:00:00', + description: 'description', + accountID: '8392101', + managerID: '8392101', + currency: 'USD', + modifiedCurrency: '', + category: 'Bananas', + tag: 'Green', + }, +]; type SearchProps = { query: string; @@ -15,6 +65,11 @@ type SearchProps = { function Search({query}: SearchProps) { const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); + useCustomBackHandler(); + const hash = SearchUtils.getQueryHash(query); const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); @@ -23,7 +78,8 @@ function Search({query}: SearchProps) { }, [query]); const isLoading = !isOffline && isLoadingOnyxValue(searchResultsMeta); - const shouldShowEmptyState = isEmptyObject(searchResults); + // Todo remove using mock data once api is done + const shouldShowEmptyState = isEmptyObject(searchResults) || !mockData; if (isLoading) { return <TableListItemSkeleton shouldAnimate />; @@ -33,15 +89,46 @@ function Search({query}: SearchProps) { return <EmptySearchView />; } + const getListHeader = () => { + if (isSmallScreenWidth) { + return; + } + + // const showMerchantColumn = ReportUtils.shouldShowMerchantColumn(data); + const showMerchantColumn = isSmallScreenWidth && true; + + return ( + <View style={[styles.flex1, styles.flexRow, styles.justifyContentBetween, styles.pl3, styles.gap3]}> + {/* <Text style={styles.searchInputStyle}>{translate('common.receipt')}</Text> */} + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.date')}</Text> + {showMerchantColumn && <Text style={[styles.searchInputStyle]}>{translate('common.merchant')}</Text>} + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.description')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.from')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.to')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.category')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.tag')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1, styles.textAlignRight]}>{translate('common.total')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.type')}</Text> + <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.action')}</Text> + </View> + ); + }; + const ListItem = SearchUtils.getListItem(); - // This will be updated with the proper List component in another PR - return SearchUtils.getSections(searchResults.data).map((item) => ( - <ListItem - key={item.transactionID} - item={item} + return ( + <SelectionList + canSelectMultiple + customListHeader={getListHeader()} + ListItem={ListItem} + sections={[{data: mockData, isDisabled: false}]} + onSelectRow={() => {}} + onSelectAll={!isSmallScreenWidth ? () => {} : undefined} + onCheckboxPress={() => {}} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} /> - )); + ); } Search.displayName = 'Search'; diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 9bd2e21bc011..13db5d6897e8 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -13,7 +13,9 @@ import TextWithTooltip from '@components/TextWithTooltip'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; import type {ListItem, TransactionListItemProps} from './types'; @@ -35,6 +37,8 @@ function TransactionListItem<TItem extends ListItem>({ const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); + const {isSmallScreenWidth} = useWindowDimensions(); + const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; @@ -47,7 +51,85 @@ function TransactionListItem<TItem extends ListItem>({ } }, [item, onCheckboxPress, onSelectRow]); - console.log('personalDetails', personalDetails); + if (isSmallScreenWidth) { + return ( + <BaseListItem + item={item} + pressableStyle={[[styles.selectionListPressableItemWrapper, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive]]} + wrapperStyle={[styles.flexColumn, styles.flex1, styles.userSelectNone, styles.alignItemsStretch]} + containerStyle={[styles.mb3]} + isFocused={isFocused} + isDisabled={isDisabled} + showTooltip={showTooltip} + canSelectMultiple={canSelectMultiple} + onSelectRow={onSelectRow} + onDismissError={onDismissError} + shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} + rightHandSideComponent={rightHandSideComponent} + errors={item.errors} + pendingAction={item.pendingAction} + keyForList={item.keyForList} + onFocus={onFocus} + shouldSyncFocus={shouldSyncFocus} + hoverStyle={item.isSelected && styles.activeComponentBG} + > + {(hovered) => ( + <> + <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween]}> + <View style={[styles.flexRow, styles.flex1, styles.alignItemsCenter, styles.gap3]}> + <View style={[styles.flexRow, styles.gap3, styles.alignItemsCenter]}> + <Avatar + imageStyles={[styles.alignSelfCenter]} + size={CONST.AVATAR_SIZE.SMALL} + source={personalDetails[item.managerID]?.avatar} + name={personalDetails[item.managerID]?.displayName} + type={CONST.ICON_TYPE_WORKSPACE} + /> + <Text + numberOfLines={1} + style={[styles.flex1, styles.flexGrow1, styles.textStrong, styles.textMicro]} + > + {personalDetails[item.managerID]?.displayName} + </Text> + </View> + <Icon + src={Expensicons.ArrowRightLong} + width={variables.iconSizeXXSmall} + height={variables.iconSizeXXSmall} + fill={theme.icon} + /> + <View style={[styles.flexRow, styles.gap3, styles.alignItemsCenter]}> + <Avatar + imageStyles={[styles.alignSelfCenter]} + size={CONST.AVATAR_SIZE.SMALL} + source={personalDetails[item.accountID]?.avatar} + name={personalDetails[item.accountID]?.displayName} + type={CONST.ICON_TYPE_WORKSPACE} + /> + <Text + numberOfLines={1} + style={[styles.flex1, styles.flexGrow1, styles.textStrong, styles.textMicro]} + > + {personalDetails[item.accountID]?.displayName} + </Text> + </View> + </View> + <View style={styles.flexShrink0}> + <Button + success + onPress={() => {}} + small + pressOnEnter + text="View" + /> + </View> + </View> + <View></View> + </> + )} + </BaseListItem> + ); + } return ( <BaseListItem @@ -93,17 +175,6 @@ function TransactionListItem<TItem extends ListItem>({ </View> </PressableWithFeedback> )} - {!!item.icons && ( - <MultipleAvatars - icons={item.icons ?? []} - shouldShowTooltip={showTooltip} - secondAvatarStyle={[ - StyleUtils.getBackgroundAndBorderStyle(theme.sidebar), - isFocused ? StyleUtils.getBackgroundAndBorderStyle(focusedBackgroundColor) : undefined, - hovered && !isFocused ? StyleUtils.getBackgroundAndBorderStyle(hoveredBackgroundColor) : undefined, - ]} - /> - )} <View style={[styles.flexRow, styles.flex1, styles.gap3]}> <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> <TextWithTooltip diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 1ae16b2eed66..619d1eee05d7 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,109 +1,26 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; -import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; -import SelectionList from '@components/SelectionList'; -import TransactionListItem from '@components/SelectionList/TransactionListItem'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import type SCREENS from '@src/SCREENS'; -// import EmptySearchView from './EmptySearchView'; -import useCustomBackHandler from './useCustomBackHandler'; type SearchPageProps = StackScreenProps<CentralPaneNavigatorParamList, typeof SCREENS.SEARCH.CENTRAL_PANE>; -const data = [ - { - receipt: {source: 'http...'}, - hasEReceipt: false, - created: '2024-04-11 00:00:00', - amount: 12500, - type: 'cash', - reportID: '1', - transactionThreadReportID: '2', - transactionID: '1234', - modifiedCreated: '2024-05-06 00:00:00', - description: 'description description description description', - accountID: '8392101', - managerID: '8392101', - currency: 'USD', - modifiedCurrency: '', - category: 'Bananas', - tag: 'Green', - }, - { - receipt: {source: 'http...'}, - hasEReceipt: false, - created: '2024-04-11 00:00:00', - amount: 12500, - type: 'cash', // not present in live data (data outside of snapshot_) - reportID: '1', - transactionThreadReportID: '2', - transactionID: '5555', - modifiedCreated: '2024-05-06 00:00:00', - description: 'description', - accountID: '8392101', - managerID: '8392101', - currency: 'USD', - modifiedCurrency: '', - category: 'Bananas', - tag: 'Green', - }, -]; - function SearchPage({route}: SearchPageProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {isSmallScreenWidth} = useWindowDimensions(); - useCustomBackHandler(); - - const getListHeader = () => { - // const showMerchantColumn = ReportUtils.shouldShowMerchantColumn(data); - const showMerchantColumn = isSmallScreenWidth && true; - - return ( - <View style={[styles.flex1, styles.flexRow, styles.justifyContentBetween, styles.pl3, styles.gap3]}> - {/* <Text style={styles.searchInputStyle}>{translate('common.receipt')}</Text> */} - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.date')}</Text> - {showMerchantColumn && <Text style={[styles.searchInputStyle]}>{translate('common.merchant')}</Text>} - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.description')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.from')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.to')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.category')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.tag')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1, styles.textAlignRight]}>{translate('common.total')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.type')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.action')}</Text> - </View> - ); - }; + const currentQuery = route?.params && 'query' in route.params ? route?.params?.query : ''; + const query = String(currentQuery); return ( - <ScreenWrapper testID={SearchPage.displayName}> + <ScreenWrapper testID={Search.displayName}> <HeaderWithBackButton title="All" icon={Illustrations.MoneyReceipts} shouldShowBackButton={false} /> - <SelectionList - canSelectMultiple - customListHeader={getListHeader()} - ListItem={TransactionListItem} - onSelectRow={() => {}} - onSelectAll={() => {}} - sections={[{data, isDisabled: false}]} - onCheckboxPress={() => {}} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} - /> - {/* <EmptySearchView /> */} + <Search query={query} />; </ScreenWrapper> ); } From 7abbdf5dd7a67992b66414ec96ebd661d26b5566 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi <someshwartripathi8@gmail.com> Date: Mon, 29 Apr 2024 16:51:06 +0530 Subject: [PATCH 519/580] Add check for logged out user --- src/pages/home/report/ReportFooter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 11d9a0a4871d..1f352ed8a440 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -141,7 +141,7 @@ function ReportFooter({ /> )} {isArchivedRoom && <ArchivedReportFooter report={report} />} - {!canWriteInReport && <SystemChatReportFooterMessage />} + {!isAnonymousUser && !canWriteInReport && <SystemChatReportFooterMessage />} {!isSmallScreenWidth && <View style={styles.offlineIndicatorRow}>{hideComposer && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}</View>} </View> )} From 26a354fe2dcb99fa5bbcf042223b23670c781244 Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Mon, 29 Apr 2024 13:58:22 +0200 Subject: [PATCH 520/580] Add SearchTransactionType --- src/CONST.ts | 6 ++++++ src/components/Search.tsx | 8 +++----- .../SelectionList/TransactionListItem.tsx | 19 +++++++++++++++++-- src/types/onyx/SearchResults.ts | 8 ++++++-- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 66b51184852f..dba9b61dc367 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4713,6 +4713,12 @@ const CONST = { MAX_TAX_RATE_INTEGER_PLACES: 4, MAX_TAX_RATE_DECIMAL_PLACES: 4, + + SEARCH_TRANSACTION_TYPE: { + CASH: 'cash', + CARD: 'card', + DISTANCE: 'distance', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 1e34eb62bebe..120bf8774cfc 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -13,9 +13,6 @@ import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import HeaderWithBackButton from './HeaderWithBackButton'; -import * as Illustrations from './Icon/Illustrations'; -import ScreenWrapper from './ScreenWrapper'; import SelectionList from './SelectionList'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; import Text from './Text'; @@ -44,7 +41,7 @@ const mockData = [ hasEReceipt: false, created: '2024-04-11 00:00:00', amount: 12500, - type: 'cash', // not present in live data (data outside of snapshot_) + type: 'card', // not present in live data (data outside of snapshot_) reportID: '1', transactionThreadReportID: '2', transactionID: '5555', @@ -68,6 +65,7 @@ function Search({query}: SearchProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); + // const [selectedCategories, setSelectedCategories] = useState<Record<string, boolean>>({}); useCustomBackHandler(); const hash = SearchUtils.getQueryHash(query); @@ -79,7 +77,7 @@ function Search({query}: SearchProps) { const isLoading = !isOffline && isLoadingOnyxValue(searchResultsMeta); // Todo remove using mock data once api is done - const shouldShowEmptyState = isEmptyObject(searchResults) || !mockData; + const shouldShowEmptyState = !isEmptyObject(searchResults) || !mockData; if (isLoading) { return <TableListItemSkeleton shouldAnimate />; diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 13db5d6897e8..c424d1e53a43 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -5,7 +5,6 @@ import Avatar from '@components/Avatar'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import MultipleAvatars from '@components/MultipleAvatars'; import {usePersonalDetails} from '@components/OnyxProvider'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; @@ -17,9 +16,23 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import {SearchTransactionType} from '@src/types/onyx/SearchResults'; import BaseListItem from './BaseListItem'; import type {ListItem, TransactionListItemProps} from './types'; +const getTypeIcon = (type: SearchTransactionType) => { + switch (type) { + case CONST.SEARCH_TRANSACTION_TYPE.CASH: + return Expensicons.Cash; + case CONST.SEARCH_TRANSACTION_TYPE.CARD: + return Expensicons.CreditCard; + case CONST.SEARCH_TRANSACTION_TYPE.DISTANCE: + return Expensicons.Car; + default: + return Expensicons.Cash; + } +}; + function TransactionListItem<TItem extends ListItem>({ item, isFocused, @@ -43,6 +56,8 @@ function TransactionListItem<TItem extends ListItem>({ const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; + const typeIcon = getTypeIcon(item.type); + const handleCheckboxPress = useCallback(() => { if (onCheckboxPress) { onCheckboxPress(item); @@ -247,7 +262,7 @@ function TransactionListItem<TItem extends ListItem>({ </View> <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> <Icon - src={Expensicons.CreditCard} + src={typeIcon} fill={theme.icon} /> </View> diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 3e8fdd191ee2..4b1127943cae 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,3 +1,5 @@ +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; import type {Receipt} from './Transaction'; type SearchResultsInfo = { @@ -22,7 +24,7 @@ type SearchTransaction = { modifiedAmount?: number; category?: string; tag?: string; - type: string; + type: SearchTransactionType; hasViolation: boolean; taxAmount?: number; reportID: string; @@ -30,6 +32,8 @@ type SearchTransaction = { action: string; }; +type SearchTransactionType = ValueOf<typeof CONST.SEARCH_TRANSACTION_TYPE>; + type SearchResults = { search: SearchResultsInfo; data: Record<string, SearchTransaction>; @@ -37,4 +41,4 @@ type SearchResults = { export default SearchResults; -export type {SearchTransaction}; +export type {SearchTransaction, SearchTransactionType}; From 8390555da6b8754f09ae255d8cd0d00ab8f5c244 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Mon, 29 Apr 2024 08:02:14 -0500 Subject: [PATCH 521/580] Create Free-plan-upgrade-to-collect-plan.md New article --- .../Free-plan-upgrade-to-collect-plan.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/articles/new-expensify/getting-started/Free-plan-upgrade-to-collect-plan.md diff --git a/docs/articles/new-expensify/getting-started/Free-plan-upgrade-to-collect-plan.md b/docs/articles/new-expensify/getting-started/Free-plan-upgrade-to-collect-plan.md new file mode 100644 index 000000000000..b8d65963c7db --- /dev/null +++ b/docs/articles/new-expensify/getting-started/Free-plan-upgrade-to-collect-plan.md @@ -0,0 +1,55 @@ +--- +title: Free Plan Upgrade to Collect Plan +description: You've been automatically upgraded to a collect plan. Here's what that means. +--- +<div id="new-expensify" markdown="1"> + +All Free plan customers have been automatically upgraded to our Collect plan. The Collect plan is a supercharged version of the Free plan you already know and love, but this upgrade unlocks several new features: +- A dedicated Setup Specialist +- Expense approvals +- Invoicing and bill pay +- Custom expense categories and tags +- Multiple active expense reports at a time +- Quickbooks Online + Xero integrations (coming soon) + +# Try it free + discount + +**The upgrade comes at no extra cost until June 1st, 2024** to give you time to try it out for free. If you add a payment card by June 1st, you’ll qualify for a special discounted rate for your first year. The discount will be applied to your Expensify bill on a sliding scale for the first 12 months (specifics of the discount are provided in the FAQs section below). + +If you have questions, reach out to your dedicated Setup Specialist using the #admins room in your chat inbox, or email us at concierge@expensify.com. + +# FAQs + +**Is the upgrade optional?** + +No, the upgrade is not optional. This upgrade ensures that every customer gets access to Expensify's best offerings and any new features going forward. + +**Does this mean Expensify will no longer be free for me?** + +Yes, but only if you want to continue using a workspace in Expensify. As always, you can still use Expensify for free without a workspace to track your expenses, chat with your friends, etc. + +**How does the sliding-scale discount work?** + +You’ll receive a discount on your Expensify bill that slowly decreases each month until you arrive at your full payment on June 1, 2025. For the first month, you’ll only pay 1/12 of the plan’s cost, and the price will gradually increase by a 12th each month. + +For example: +- July 1: The discount covers 91.667% of your bill (11/12). +- August 1: The discount covers 83.333% of your bill (10/12). +- September 1: The discount covers 75% of your bill (9/12). +- October 1: The discount covers 66.7% of your bill (8/12). +- November 1: The discount covers 58.3% of your bill (7/12). +- December 1: The discount covers 50% of your bill (6/12). +- January 1: The discount covers 41.667% of your bill (5/12). +- February 1: The discount covers 33.333% of your bill (4/12). +- March 1: The discount covers 25% of your bill (3/12). +- April 1: The discount covers 16.667% of your bill (2/12). +- May 1: The discount covers 8.333% of your bill (1/12). +- June 1: You pay the full bill amount from this month forward. + +The discount will be reflected on your monthly Expensify bill as a “Workspace upgrade discount.” The discount can be combined with any other offers you qualify for, including our annual subscription discount, Expensify Card discount, and Expensify Card cash back. + +**How do I get in touch with my Setup Specialist?** + +You can reach your Setup Specialist by opening your workspace’s #admins room in your chat inbox and sending a message. + +</div> From 43710829df28df950ce6b2364ccfade09f4831b3 Mon Sep 17 00:00:00 2001 From: VickyStash <vikstash@gmail.com> Date: Mon, 29 Apr 2024 15:28:56 +0200 Subject: [PATCH 522/580] Remove quick action update during invoice creation --- src/libs/actions/IOU.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4d7287b1cef8..820481844589 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -869,15 +869,6 @@ function buildOnyxDataForInvoice( [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, }, }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, - value: { - action: CONST.QUICK_ACTIONS.REQUEST_MANUAL, - chatReportID: chatReport?.reportID, - isFirstQuickAction: isEmptyObject(quickAction), - }, - }, // Remove the temporary transaction used during the creation flow { onyxMethod: Onyx.METHOD.SET, From 1969563898b6cd160944b5a13e6b3dd87b4af0a0 Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Mon, 29 Apr 2024 15:33:02 +0200 Subject: [PATCH 523/580] handle clicking on TransactionListItem --- src/components/Search.tsx | 16 +++++++++++++--- .../SelectionList/TransactionListItem.tsx | 10 ++++++---- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 120bf8774cfc..fd2232a29fcd 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -8,9 +8,11 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as SearchActions from '@libs/actions/Search'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as SearchUtils from '@libs/SearchUtils'; +import Navigation from '@navigation/Navigation'; import EmptySearchView from '@pages/Search/EmptySearchView'; import useCustomBackHandler from '@pages/Search/useCustomBackHandler'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import SelectionList from './SelectionList'; @@ -24,7 +26,7 @@ const mockData = [ created: '2024-04-11 00:00:00', amount: 12500, type: 'cash', - reportID: '1', + reportID: '3632789879960357', transactionThreadReportID: '2', transactionID: '1234', modifiedCreated: '2024-05-06 00:00:00', @@ -42,7 +44,7 @@ const mockData = [ created: '2024-04-11 00:00:00', amount: 12500, type: 'card', // not present in live data (data outside of snapshot_) - reportID: '1', + reportID: '5768873634031661', transactionThreadReportID: '2', transactionID: '5555', modifiedCreated: '2024-05-06 00:00:00', @@ -112,6 +114,12 @@ function Search({query}: SearchProps) { ); }; + const openReport = (reportID?: string) => { + if (reportID) { + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(query, reportID)); + } + }; + const ListItem = SearchUtils.getListItem(); return ( @@ -120,7 +128,9 @@ function Search({query}: SearchProps) { customListHeader={getListHeader()} ListItem={ListItem} sections={[{data: mockData, isDisabled: false}]} - onSelectRow={() => {}} + onSelectRow={(item) => { + openReport(item.reportID); + }} onSelectAll={!isSmallScreenWidth ? () => {} : undefined} onCheckboxPress={() => {}} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index c424d1e53a43..6a90ffb3892f 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -16,7 +16,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import {SearchTransactionType} from '@src/types/onyx/SearchResults'; +import type {SearchTransactionType} from '@src/types/onyx/SearchResults'; import BaseListItem from './BaseListItem'; import type {ListItem, TransactionListItemProps} from './types'; @@ -77,7 +77,7 @@ function TransactionListItem<TItem extends ListItem>({ isDisabled={isDisabled} showTooltip={showTooltip} canSelectMultiple={canSelectMultiple} - onSelectRow={onSelectRow} + onSelectRow={() => {}} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} @@ -156,7 +156,7 @@ function TransactionListItem<TItem extends ListItem>({ isDisabled={isDisabled} showTooltip={showTooltip} canSelectMultiple={canSelectMultiple} - onSelectRow={onSelectRow} + onSelectRow={() => {}} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} @@ -270,7 +270,9 @@ function TransactionListItem<TItem extends ListItem>({ <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> <Button success - onPress={() => {}} + onPress={() => { + onSelectRow(item); + }} small pressOnEnter text="View" From 19c976e2d0d2f55d9cae30539b0cf05dd6a328d4 Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Mon, 29 Apr 2024 16:50:52 +0200 Subject: [PATCH 524/580] Add flex1 to views with text --- src/components/SelectionList/TransactionListItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 6a90ffb3892f..af84c4494700 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -92,7 +92,7 @@ function TransactionListItem<TItem extends ListItem>({ <> <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween]}> <View style={[styles.flexRow, styles.flex1, styles.alignItemsCenter, styles.gap3]}> - <View style={[styles.flexRow, styles.gap3, styles.alignItemsCenter]}> + <View style={[styles.flexRow, styles.flex1, styles.gap3, styles.alignItemsCenter, styles.justifyContentBetween]}> <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} @@ -113,7 +113,7 @@ function TransactionListItem<TItem extends ListItem>({ height={variables.iconSizeXXSmall} fill={theme.icon} /> - <View style={[styles.flexRow, styles.gap3, styles.alignItemsCenter]}> + <View style={[styles.flexRow, styles.flex1, styles.gap3, styles.alignItemsCenter]}> <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} From 3940e24ba113225db59a8c8e803c97e3fba4a176 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar <monil@expensify.com> Date: Mon, 29 Apr 2024 20:36:39 +0530 Subject: [PATCH 525/580] Correct default parameter --- src/libs/SidebarUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 1fa7844fe6b1..246dccec4662 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -413,7 +413,7 @@ function getOptionData({ result.subtitle = subtitle; result.participantsList = participantPersonalDetailList; - result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail?.avatar ?? {}, personalDetail?.accountID), '', -1, policy); + result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail?.avatar ?? '', personalDetail?.accountID), '', -1, policy); result.searchText = OptionsListUtils.getSearchText(report, reportName, participantPersonalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); result.displayNamesWithTooltips = displayNamesWithTooltips; From 8d24dc0598654b467510a08e2c710eec0668d53e Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:23:06 +0200 Subject: [PATCH 526/580] apply fix for QBO set up crash --- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index cc724d00fe1d..d8edc96d3d05 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -102,7 +102,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro titleTranslationKey: 'workspace.moreFeatures.taxes.title', subtitleTranslationKey: 'workspace.moreFeatures.taxes.subtitle', isActive: (policy?.tax?.trackingEnabled ?? false) || isSyncTaxEnabled, - disabled: isSyncTaxEnabled || policy?.connections?.quickbooksOnline?.data.country === CONST.COUNTRY.US, + disabled: isSyncTaxEnabled || policy?.connections?.quickbooksOnline?.data?.country === CONST.COUNTRY.US, pendingAction: policy?.pendingFields?.tax, action: (isEnabled: boolean) => { Policy.enablePolicyTaxes(policy?.id ?? '', isEnabled); From a49ab76f97fc2c4ab3dc31917648dd31de72b6df Mon Sep 17 00:00:00 2001 From: Puneet Lath <puneet@expensify.com> Date: Mon, 29 Apr 2024 11:39:22 -0400 Subject: [PATCH 527/580] Revert "Add error for failure invited member" --- src/languages/en.ts | 3 --- src/languages/es.ts | 3 --- src/libs/actions/Report.ts | 34 +++++++++------------------------- src/pages/RoomMembersPage.tsx | 10 ---------- src/types/onyx/Report.ts | 1 - 5 files changed, 9 insertions(+), 42 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 1fd86608222c..8cc863d8be68 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2425,9 +2425,6 @@ export default { memberNotFound: 'Member not found. To invite a new member to the room, please use the Invite button above.', notAuthorized: `You do not have access to this page. Are you trying to join the room? Please reach out to a member of this room so they can add you as a member! Something else? Reach out to ${CONST.EMAIL.CONCIERGE}`, removeMembersPrompt: 'Are you sure you want to remove the selected members from the room?', - error: { - genericAdd: 'There was a problem adding this room member.', - }, }, newTaskPage: { assignTask: 'Assign task', diff --git a/src/languages/es.ts b/src/languages/es.ts index b32b2cbc93b8..eab9e23d9b03 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2456,9 +2456,6 @@ export default { memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro a la sala de chat, por favor, utiliza el botón Invitar que está más arriba.', notAuthorized: `No tienes acceso a esta página. ¿Estás tratando de unirte a la sala de chat? Comunícate con el propietario de esta sala de chat para que pueda añadirte como miembro. ¿Necesitas algo más? Comunícate con ${CONST.EMAIL.CONCIERGE}`, removeMembersPrompt: '¿Estás seguro de que quieres eliminar a los miembros seleccionados de la sala de chat?', - error: { - genericAdd: 'Hubo un problema al añadir este miembro a la sala de chat.', - }, }, newTaskPage: { assignTask: 'Asignar tarea', diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 98300b563e29..b3f1a9203bc9 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2652,18 +2652,17 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - pendingChatMembers: - pendingChatMembers.map((pendingChatMember) => { - if (!inviteeAccountIDs.includes(Number(pendingChatMember.accountID))) { - return pendingChatMember; - } - return { - ...pendingChatMember, - errors: ErrorUtils.getMicroSecondOnyxError('roomMembersPage.error.genericAdd'), - }; - }) ?? null, + participantAccountIDs: report.participantAccountIDs, + visibleChatMemberAccountIDs: report.visibleChatMemberAccountIDs, + participants: inviteeAccountIDs.reduce((revertedParticipants: Record<number, null>, accountID) => { + // eslint-disable-next-line no-param-reassign + revertedParticipants[accountID] = null; + return revertedParticipants; + }, {}), + pendingChatMembers: report?.pendingChatMembers ?? null, }, }, + ...newPersonalDetailsOnyxData.finallyData, ]; if (ReportUtils.isGroupChat(report)) { @@ -2686,20 +2685,6 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: InvitedEmails API.write(WRITE_COMMANDS.INVITE_TO_ROOM, parameters, {optimisticData, successData, failureData}); } -function clearAddRoomMemberError(reportID: string, invitedAccountID: string) { - const report = currentReportData?.[reportID]; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - pendingChatMembers: report?.pendingChatMembers?.filter((pendingChatMember) => pendingChatMember.accountID !== invitedAccountID), - participantAccountIDs: report?.parentReportActionIDs?.filter((parentReportActionID) => parentReportActionID !== Number(invitedAccountID)), - participants: { - [invitedAccountID]: null, - }, - }); - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { - [invitedAccountID]: null, - }); -} - function updateGroupChatMemberRoles(reportID: string, accountIDList: number[], role: ValueOf<typeof CONST.REPORT.ROLE>) { const participants: Participants = {}; const memberRoles: Record<number, string> = {}; @@ -3784,5 +3769,4 @@ export { leaveGroupChat, removeFromGroupChat, updateGroupChatMemberRoles, - clearAddRoomMemberError, }; diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index 67b6d96f182d..488bde658c3f 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -216,7 +216,6 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { }, ], pendingAction: pendingChatMember?.pendingAction, - errors: pendingChatMember?.errors, }); }); @@ -225,13 +224,6 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { return result; }; - const dismissError = useCallback( - (item: ListItem) => { - Report.clearAddRoomMemberError(report.reportID, String(item.accountID ?? '')); - }, - [report.reportID], - ); - const isPolicyEmployee = useMemo(() => { if (!report?.policyID || policies === null) { return false; @@ -240,7 +232,6 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { }, [report?.policyID, policies]); const data = getMemberOptions(); const headerMessage = searchValue.trim() && !data.length ? translate('roomMembersPage.memberNotFound') : ''; - return ( <ScreenWrapper includeSafeAreaPaddingBottom={false} @@ -309,7 +300,6 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { showScrollIndicator shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} ListItem={UserListItem} - onDismissError={dismissError} /> </View> </View> diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index f37a0c3655f3..ec1cccf19faf 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -21,7 +21,6 @@ type Note = OnyxCommon.OnyxValueWithOfflineFeedback<{ type PendingChatMember = { accountID: string; pendingAction: OnyxCommon.PendingAction; - errors?: OnyxCommon.Errors; }; type Participant = { From cda577711ecc0d5fac27f3893a6dd84788fd541f Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Mon, 29 Apr 2024 17:42:36 +0200 Subject: [PATCH 528/580] improve TransactionListItem on narrow screens --- .../SelectionList/TransactionListItem.tsx | 121 ++++++++++-------- 1 file changed, 70 insertions(+), 51 deletions(-) diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index af84c4494700..8981cf87ec2a 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -50,7 +50,7 @@ function TransactionListItem<TItem extends ListItem>({ const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); - const {isSmallScreenWidth} = useWindowDimensions(); + const {isMediumScreenWidth, isSmallScreen} = useWindowDimensions(); const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; @@ -66,7 +66,53 @@ function TransactionListItem<TItem extends ListItem>({ } }, [item, onCheckboxPress, onSelectRow]); - if (isSmallScreenWidth) { + const rowButtonElement = ( + <Button + success + onPress={() => { + onSelectRow(item); + }} + small + pressOnEnter + text="View" + /> + ); + + const amountElement = ( + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={`${CurrencyUtils.getLocalizedCurrencySymbol(item.currency)}${item.amount}`} + style={[styles.optionDisplayName, styles.textNewKansasNormal, styles.pre, styles.justifyContentCenter]} + /> + ); + + const categoryElement = ( + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.category} + style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} + /> + ); + + const descriptionElement = ( + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={item.description} + style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} + /> + ); + + const dateElement = ( + <TextWithTooltip + shouldShowTooltip={showTooltip} + text={format(new Date(item.created), 'MMM dd')} + style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} + /> + ); + + const displayNarrowVersion = isMediumScreenWidth || isSmallScreen; + + if (displayNarrowVersion) { return ( <BaseListItem item={item} @@ -90,7 +136,7 @@ function TransactionListItem<TItem extends ListItem>({ > {(hovered) => ( <> - <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween]}> + <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween, styles.mb2]}> <View style={[styles.flexRow, styles.flex1, styles.alignItemsCenter, styles.gap3]}> <View style={[styles.flexRow, styles.flex1, styles.gap3, styles.alignItemsCenter, styles.justifyContentBetween]}> <Avatar @@ -129,17 +175,24 @@ function TransactionListItem<TItem extends ListItem>({ </Text> </View> </View> - <View style={styles.flexShrink0}> - <Button - success - onPress={() => {}} - small - pressOnEnter - text="View" - /> + <View style={styles.flexShrink0}>{rowButtonElement}</View> + </View> + <View style={[styles.flexRow, styles.justifyContentBetween]}> + <View> + {descriptionElement} + {categoryElement} + </View> + <View> + {amountElement} + <View style={[styles.flex1, styles.flexRow, styles.alignItemsStretch, styles.gap2]}> + <Icon + src={typeIcon} + fill={theme.icon} + /> + {dateElement} + </View> </View> </View> - <View></View> </> )} </BaseListItem> @@ -191,20 +244,8 @@ function TransactionListItem<TItem extends ListItem>({ </PressableWithFeedback> )} <View style={[styles.flexRow, styles.flex1, styles.gap3]}> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={format(new Date(item.created), 'MMM dd')} - style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} - /> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.description} - style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} - /> - </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>{dateElement}</View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>{descriptionElement}</View> <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> <View style={[styles.flexRow, styles.gap3, styles.flex1, styles.alignItemsCenter]}> <Avatar @@ -239,13 +280,7 @@ function TransactionListItem<TItem extends ListItem>({ </Text> </View> </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={item.category} - style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} - /> - </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>{categoryElement}</View> <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> <TextWithTooltip shouldShowTooltip={showTooltip} @@ -253,13 +288,7 @@ function TransactionListItem<TItem extends ListItem>({ style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} /> </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsEnd]}> - <TextWithTooltip - shouldShowTooltip={showTooltip} - text={`${CurrencyUtils.getLocalizedCurrencySymbol(item.currency)}${item.amount}`} - style={[styles.optionDisplayName, styles.textNewKansasNormal, styles.pre, styles.justifyContentCenter]} - /> - </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsEnd]}>{amountElement}</View> <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> <Icon src={typeIcon} @@ -267,17 +296,7 @@ function TransactionListItem<TItem extends ListItem>({ /> </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <Button - success - onPress={() => { - onSelectRow(item); - }} - small - pressOnEnter - text="View" - /> - </View> + <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>{rowButtonElement}</View> </View> {!!item.rightElement && item.rightElement} </> From 62a8b6977043a4d16441d9af966d16558e57d105 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:07:47 -0400 Subject: [PATCH 529/580] Update Insights.md fixed FAQ section --- .../spending-insights/Insights.md | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/docs/articles/expensify-classic/spending-insights/Insights.md b/docs/articles/expensify-classic/spending-insights/Insights.md index ce07f4b56450..052ea8b13395 100644 --- a/docs/articles/expensify-classic/spending-insights/Insights.md +++ b/docs/articles/expensify-classic/spending-insights/Insights.md @@ -4,16 +4,16 @@ description: How to get the most out of the Custom Reporing and Insights redirect_from: articles/other/Insights/ --- <!-- The lines above are required by Jekyll to process the .md file --> -{% raw %} -# What is Custom Reporting and Insights? -The Insights dashboard allows you to monitor all aspects of company spend across categories, employees, projects, departments, and more. You can see trends in real time, forecast company budgets, and build unlimited custom reports with help from our trained specialist team. + +# Overview +The Insights dashboard allows you to monitor all aspects of company spending across categories, employees, projects, departments, and more. You can see trends in real-time, forecast company budgets, and build unlimited custom reports with help from our trained specialist team. ![Insights Pie Chart](https://help.expensify.com/assets/images/insights-chart.png){:width="100%"} ## Review your Insights data -1. Navigate to your [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}), located in the left hand menu +1. Navigate to your [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}), located in the left-hand menu 2. Select a specific date range (the default view has the current month pre-selected) -3. Use the filter options to select the categories, tags, employees etc that you want insights on +3. Use the filter options to select the categories, tags, employees, or any other parameter 4. Make sure that View in the top right corner is set to the pie chart icon 5. You can view any dataset in more detail by clicking in the “View Raw Data” column @@ -21,7 +21,7 @@ The Insights dashboard allows you to monitor all aspects of company spend across 1. Switch the View in the top right corner of the [Insights page](https://www.expensify.com/expenses?param={"fromInsightsTab":true,"viewMode":"charts"}) to the lists icon 2. Select the expenses you want to export, either by selecting individual expenses, or checking the select all box (next to Date at the top) -3. Select **Export To** in the top right hand corner to download the report as a .csv file +3. Select **Export To** in the top right-hand corner to download the report as a .csv file ## Create a Custom Export Report for your Expenses @@ -43,28 +43,28 @@ If you would like to create a custom export report that can be shared with other #### Can I put expenses from different policies on the same report? -Custom export reports created under Settings > Account > Preferences page are able to export expenses from multiple policies, and custom export formats created under Settings > Policies > Group > [Policy Name] > Export Formats are for expenses reported under that policy only. +Custom export reports created under the Settings > Account > Preferences page can export expenses from multiple policies, and custom export formats created under Settings > Workspaces> Group > [Workspace Name] > Export Formats are for expenses reported under that policy only. #### Are there any default export reports available? Yes! We have [seven default reports](https://community.expensify.com/discussion/5602/deep-dive-default-export-templates) available to export directly from the Reports page: - **All Data** - Expense Level Export** - the name says it all! This is for the people who want ALL the details from their expense reports. We're talking Tax, Merchant Category Codes, Approvers - you name it, this report's got it! -- **All Data** - Report Level Export - this is the report for those who don't need to see each individual expense but want to see a line by line breakdown at a report level - submitter, total amount, report ID - that kind of stuff +- **All Data** - Report Level Export - this is the report for those who don't need to see each individual expense but want to see a line-by-line breakdown at a report level - submitter, total amount, report ID - that kind of stuff - **Basic Export** - this is the best way to get a simple breakdown of all your expenses - just the basics - **Canadian Multiple Tax Export** - tax, GST, PST...if you need to know tax then this is the export you want! - **Category Export** - want to see a breakdown of your expenses by Category? This is the export you - **Per Diem Export** - the name says it all - **Tag Export** - much like the Category Export, but for Tags -*To note: these reports will be emailed directly to your email address rather than downloaded on your computer.* +*These reports will be emailed directly to your email address rather than automatically downloaded.* #### How many expenses can I export in one report? The custom export reports are best for small-to-medium chunks of data. If you want to export large amounts of data, we recommend you use a [default export report](https://community.expensify.com/discussion/5602/deep-dive-default-export-templates) that you can run from the Reports page. #### What other kinds of export reports can my Account Manager help me create? -We’ve built a huge variety of custom reports for customers, so make sure to reach out to your Account Manager for more details. Some examples of custom reports we’ve build for customers before are: +We’ve built a huge variety of custom reports for customers, so make sure to reach out to your Account Manager for more details. Some examples of custom reports we’ve built for customers before are: - Accrual Report - Aged Approval Reports @@ -97,7 +97,5 @@ We’ve built a huge variety of custom reports for customers, so make sure to re - Unposted Procurement Aging Report - Unposted Travel Aging Report - Vendor Spend -- … or anything you can imagine! -{% endraw %} {% include faq-end.md %} From 2ad1e72dc938ab5476eb346c30b598031e29e376 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:13:45 +0200 Subject: [PATCH 530/580] apply corrected translations --- .../index.native.tsx | 2 +- .../ConnectToQuickbooksOnlineButton/index.tsx | 2 +- .../ConnectToXeroButton/index.native.tsx | 2 +- src/components/ConnectToXeroButton/index.tsx | 2 +- src/languages/en.ts | 28 +++++++++++++++---- src/languages/es.ts | 28 +++++++++++++++---- .../accounting/PolicyAccountingPage.tsx | 4 +-- 7 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx index a17e550e14cf..3a5e545cce88 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.native.tsx @@ -52,7 +52,7 @@ function ConnectToQuickbooksOnlineButton({ /> {shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect && isDisconnectModalOpen && ( <ConfirmModal - title={translate('workspace.accounting.disconnectTitle')} + title={translate('workspace.accounting.disconnectTitle', CONST.POLICY.CONNECTIONS.NAME.XERO)} onConfirm={() => { removePolicyConnection(policyID, integrationToDisconnect); setIsDisconnectModalOpen(false); diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.tsx index 51f714c5539e..e81332120fb8 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.tsx @@ -33,7 +33,7 @@ function ConnectToQuickbooksOnlineButton({policyID, shouldDisconnectIntegrationB /> {shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect && isDisconnectModalOpen && ( <ConfirmModal - title={translate('workspace.accounting.disconnectTitle')} + title={translate('workspace.accounting.disconnectTitle', CONST.POLICY.CONNECTIONS.NAME.XERO)} isVisible={isDisconnectModalOpen} onConfirm={() => { removePolicyConnection(policyID, integrationToDisconnect); diff --git a/src/components/ConnectToXeroButton/index.native.tsx b/src/components/ConnectToXeroButton/index.native.tsx index e963233c2f34..36c5af4a0575 100644 --- a/src/components/ConnectToXeroButton/index.native.tsx +++ b/src/components/ConnectToXeroButton/index.native.tsx @@ -49,7 +49,7 @@ function ConnectToXeroButton({policyID, session, shouldDisconnectIntegrationBefo /> {shouldDisconnectIntegrationBeforeConnecting && isDisconnectModalOpen && integrationToDisconnect && ( <ConfirmModal - title={translate('workspace.accounting.disconnectTitle')} + title={translate('workspace.accounting.disconnectTitle', CONST.POLICY.CONNECTIONS.NAME.QBO)} onConfirm={() => { removePolicyConnection(policyID, integrationToDisconnect); setIsDisconnectModalOpen(false); diff --git a/src/components/ConnectToXeroButton/index.tsx b/src/components/ConnectToXeroButton/index.tsx index 75d41cceec3f..8fad63e1a965 100644 --- a/src/components/ConnectToXeroButton/index.tsx +++ b/src/components/ConnectToXeroButton/index.tsx @@ -33,7 +33,7 @@ function ConnectToXeroButton({policyID, shouldDisconnectIntegrationBeforeConnect /> {shouldDisconnectIntegrationBeforeConnecting && isDisconnectModalOpen && integrationToDisconnect && ( <ConfirmModal - title={translate('workspace.accounting.disconnectTitle')} + title={translate('workspace.accounting.disconnectTitle', CONST.POLICY.CONNECTIONS.NAME.QBO)} isVisible onConfirm={() => { removePolicyConnection(policyID, integrationToDisconnect); diff --git a/src/languages/en.ts b/src/languages/en.ts index dbc3548882af..462cfe9965f9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2168,15 +2168,33 @@ export default { other: 'Other integrations', syncNow: 'Sync now', disconnect: 'Disconnect', - disconnectTitle: 'Disconnect integration', - disconnectPrompt: (integrationToConnect?: ConnectionName): string => { + disconnectTitle: (integration?: ConnectionName): string => { + switch (integration) { + case CONST.POLICY.CONNECTIONS.NAME.QBO: + return 'Disconnect QuickBooks Online'; + case CONST.POLICY.CONNECTIONS.NAME.XERO: + return 'Disconnect Xero'; + default: { + return 'Disconnect integration'; + } + } + }, + disconnectPrompt: (integrationToConnect?: ConnectionName, currentIntegration?: ConnectionName): string => { switch (integrationToConnect) { case CONST.POLICY.CONNECTIONS.NAME.QBO: - return 'To set up QuickBooks Online you must disconnect. Are you sure you want to disconnect this integration?'; + return 'Are you sure you want to disconnect Xero to set up QuickBooks Online?'; case CONST.POLICY.CONNECTIONS.NAME.XERO: - return 'To set up Xero you must disconnect. Are you sure you want to disconnect this integration?'; + return 'Are you sure you want to disconnect QuickBooks Online to set up Xero?'; default: { - return 'Are you sure you want to disconnect this integration?'; + switch (currentIntegration) { + case CONST.POLICY.CONNECTIONS.NAME.QBO: + return 'Are you sure you want to disconnect QuickBooks Online?'; + case CONST.POLICY.CONNECTIONS.NAME.XERO: + return 'Are you sure you want to disconnect Xero?'; + default: { + return 'Are you sure you want to disconnect this integration?'; + } + } } } }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 849aef9ef968..617b4fc4cdf5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2163,15 +2163,33 @@ export default { other: 'Otras integraciones', syncNow: 'Sincronizar ahora', disconnect: 'Desconectar', - disconnectTitle: 'Desconectar integración', - disconnectPrompt: (integrationToConnect?: ConnectionName): string => { + disconnectTitle: (currentIntegration?: ConnectionName): string => { + switch (currentIntegration) { + case CONST.POLICY.CONNECTIONS.NAME.QBO: + return 'Desconectar QuickBooks Online'; + case CONST.POLICY.CONNECTIONS.NAME.XERO: + return 'Desconectar Xero'; + default: { + return 'Desconectar integración'; + } + } + }, + disconnectPrompt: (integrationToConnect?: ConnectionName, currentIntegration?: ConnectionName): string => { switch (integrationToConnect) { case CONST.POLICY.CONNECTIONS.NAME.QBO: - return 'Para configurar QuickBooks Online debe desconectarse. ¿Estás seguro de que deseas desconectar esta intregración?'; + return '¿Estás seguro de que quieres desconectar Xero para configurar QuickBooks Online?'; case CONST.POLICY.CONNECTIONS.NAME.XERO: - return 'Para configurar Xero debe desconectarse. ¿Estás seguro de que deseas desconectar esta intregración?'; + return '¿Estás seguro de que quieres desconectar QuickBooks Online para configurar Xero?'; default: { - return '¿Estás seguro de que deseas desconectar esta intregración?'; + switch (currentIntegration) { + case CONST.POLICY.CONNECTIONS.NAME.QBO: + return '¿Estás seguro de que quieres desconectar QuickBooks Online?'; + case CONST.POLICY.CONNECTIONS.NAME.XERO: + return '¿Estás seguro de que quieres desconectar Xero?'; + default: { + return '¿Estás seguro de que quieres desconectar integración?'; + } + } } } }, diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 53727d6a8502..aa2fdf20313d 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -315,7 +315,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting </View> </ScrollView> <ConfirmModal - title={translate('workspace.accounting.disconnectTitle')} + title={translate('workspace.accounting.disconnectTitle', connectedIntegration)} isVisible={isDisconnectModalOpen} onConfirm={() => { if (connectedIntegration) { @@ -324,7 +324,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting setIsDisconnectModalOpen(false); }} onCancel={() => setIsDisconnectModalOpen(false)} - prompt={translate('workspace.accounting.disconnectPrompt')} + prompt={translate('workspace.accounting.disconnectPrompt', undefined, connectedIntegration)} confirmText={translate('workspace.accounting.disconnect')} cancelText={translate('common.cancel')} danger From 156b177724e3ccb96b6abbb3868a9707d167fac4 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:21:48 -0400 Subject: [PATCH 531/580] Update Insights.md additional updates to Insights article --- .../spending-insights/Insights.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/articles/expensify-classic/spending-insights/Insights.md b/docs/articles/expensify-classic/spending-insights/Insights.md index 052ea8b13395..c5ee218352fd 100644 --- a/docs/articles/expensify-classic/spending-insights/Insights.md +++ b/docs/articles/expensify-classic/spending-insights/Insights.md @@ -26,28 +26,28 @@ The Insights dashboard allows you to monitor all aspects of company spending acr ## Create a Custom Export Report for your Expenses 1. Navigate to **Settings > Account > Preferences > scroll down to CSV Export Formats** -2. Build up a report using these [formulas](https://community.expensify.com/discussion/5795/deep-dive-expense-level-formula/p1?new=1) +2. Build up a report using these [formulas]((https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates)) 3. Click the **Custom Export** button on the Insights page and your Account Manager will help get you started on building up your report -## Create a Custom Export Report for your Policy +## Create a Custom Export Report for your Workspace -1. Navigate to **Settings > Policies > Group > [Policy Name] > Export Formats** -2. Build up a report using these [formulas](https://community.expensify.com/discussion/5795/deep-dive-expense-level-formula/p1?new=1) +1. Navigate to **Settings > Workspaces > Group > [Workspace Name] > Export Formats** +2. Build up a report using these [formulas](https://help.expensify.com/articles/expensify-classic/spending-insights/Custom-Templates) 3. If you need any help, click the **Support** button on the top left to contact your Account Manager {% include faq-begin.md %} #### Can I share my custom export report? -If you would like to create a custom export report that can be shared with other policy admins, you can create these by navigating to the **[Settings > Policies > Group > [Policy Name] > Export Formats](https://www.expensify.com/admin_policies?param={"section":"group"})** page. Custom export reports created under **Settings > Account > Preferences** page are only available to the member who created them. +If you would like to create a custom export report that can be shared with other workspace admins, you can do so by navigating to the **[Settings > Workspaces > Group > [Workspace Name] > Export Formats** page. Custom export reports created under the **Settings > Account > Preferences** page are only available to the member who created them. -#### Can I put expenses from different policies on the same report? +#### Can I put expenses from different workspaces on the same report? -Custom export reports created under the Settings > Account > Preferences page can export expenses from multiple policies, and custom export formats created under Settings > Workspaces> Group > [Workspace Name] > Export Formats are for expenses reported under that policy only. +Custom export reports created under the Settings > Account > Preferences page can export expenses from multiple workspaces, and custom export formats created under Settings > Workspaces> Group > [Workspace Name] > Export Formats are for expenses reported under that workspace only. #### Are there any default export reports available? -Yes! We have [seven default reports](https://community.expensify.com/discussion/5602/deep-dive-default-export-templates) available to export directly from the Reports page: +Yes! We have [seven default reports](https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates) available to export directly from the Reports page: - **All Data** - Expense Level Export** - the name says it all! This is for the people who want ALL the details from their expense reports. We're talking Tax, Merchant Category Codes, Approvers - you name it, this report's got it! - **All Data** - Report Level Export - this is the report for those who don't need to see each individual expense but want to see a line-by-line breakdown at a report level - submitter, total amount, report ID - that kind of stuff @@ -60,7 +60,7 @@ Yes! We have [seven default reports](https://community.expensify.com/discussion/ *These reports will be emailed directly to your email address rather than automatically downloaded.* #### How many expenses can I export in one report? -The custom export reports are best for small-to-medium chunks of data. If you want to export large amounts of data, we recommend you use a [default export report](https://community.expensify.com/discussion/5602/deep-dive-default-export-templates) that you can run from the Reports page. +The custom export reports are best for small-to-medium chunks of data. If you want to export large amounts of data, we recommend you use a [default export report](https://help.expensify.com/articles/expensify-classic/spending-insights/Default-Export-Templates) that you can run from the Reports page. #### What other kinds of export reports can my Account Manager help me create? From 944db28907f5835a8e845c78b8104c42a0b36437 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Mon, 29 Apr 2024 16:27:11 +0000 Subject: [PATCH 532/580] Update version to 1.4.67-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d5afad11a9fa..b3e8170ca986 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046700 - versionName "1.4.67-0" + versionCode 1001046701 + versionName "1.4.67-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4d2e7ba3b992..5f016311eff3 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.67.0</string> + <string>1.4.67.1</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 952fbeddd75e..d249f6d88c65 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.67.0</string> + <string>1.4.67.1</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index d6920d746496..b7d75cd6f03d 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.67</string> <key>CFBundleVersion</key> - <string>1.4.67.0</string> + <string>1.4.67.1</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index 47737d0223d7..b989563b8138 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.67-0", + "version": "1.4.67-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.67-0", + "version": "1.4.67-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b01a03dc109f..1eed2fe08600 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.67-0", + "version": "1.4.67-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 04df83fafde81034242711b10f0563775d86cd5e Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Mon, 29 Apr 2024 18:33:52 +0200 Subject: [PATCH 533/580] add the displayName property back to the ConnectToQuickbooksOnlineButton component --- src/components/ConnectToQuickbooksOnlineButton/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/ConnectToQuickbooksOnlineButton/index.tsx b/src/components/ConnectToQuickbooksOnlineButton/index.tsx index e81332120fb8..7de2ba55fb84 100644 --- a/src/components/ConnectToQuickbooksOnlineButton/index.tsx +++ b/src/components/ConnectToQuickbooksOnlineButton/index.tsx @@ -51,4 +51,6 @@ function ConnectToQuickbooksOnlineButton({policyID, shouldDisconnectIntegrationB ); } +ConnectToQuickbooksOnlineButton.displayName = 'ConnectToQuickbooksOnlineButton'; + export default ConnectToQuickbooksOnlineButton; From a52d6b3ad20576f479cad78e73b418ee3975523c Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi <someshwartripathi8@gmail.com> Date: Mon, 29 Apr 2024 22:33:52 +0530 Subject: [PATCH 534/580] Add system chatType --- src/CONST.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CONST.ts b/src/CONST.ts index 66b51184852f..41eb6b1e760d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -56,6 +56,7 @@ const chatTypes = { POLICY_EXPENSE_CHAT: 'policyExpenseChat', SELF_DM: 'selfDM', INVOICE: 'invoice', + SYSTEM: 'system', } as const; // Explicit type annotation is required From ccb4dfc9a3be057dc351f27d0b3f37f61b32f723 Mon Sep 17 00:00:00 2001 From: Jack Nam <jack@expensify.com> Date: Mon, 29 Apr 2024 10:11:34 -0700 Subject: [PATCH 535/580] Update IOU.ts --- src/libs/actions/IOU.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 07ff9420b60e..313fb2936d6e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2539,6 +2539,7 @@ const getConvertTrackedExpenseInformation = ( function convertTrackedExpenseToRequest( payerAccountID: number, + payerEmail: string, chatReportID: string, transactionID: string, actionableWhisperReportActionID: string, @@ -2587,6 +2588,7 @@ function convertTrackedExpenseToRequest( merchant, receipt, payerAccountID, + payerEmail, chatReportID, transactionID, actionableWhisperReportActionID, @@ -2820,6 +2822,7 @@ function requestMoney( convertTrackedExpenseToRequest( payerAccountID, + payerEmail, chatReport.reportID, transaction.transactionID, actionableWhisperReportActionID, From 85bca6a14835fd822969e879406c4ffe2e6a6497 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi <someshwartripathi8@gmail.com> Date: Mon, 29 Apr 2024 22:41:54 +0530 Subject: [PATCH 536/580] Add isSystemChat util function --- src/libs/ReportUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a0da34416aad..481977a53784 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -993,6 +993,10 @@ function isGroupChat(report: OnyxEntry<Report> | Partial<Report>): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.GROUP; } +function isSystemChat(report: OnyxEntry<Report>): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.SYSTEM; +} + /** * Only returns true if this is our main 1:1 DM report with Concierge */ @@ -6536,6 +6540,7 @@ export { isReportParticipant, isSelfDM, isSettled, + isSystemChat, isTaskReport, isThread, isThreadFirstChat, From c8e9484cc1c370156ff4069f204c69e6bf32a246 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi <someshwartripathi8@gmail.com> Date: Mon, 29 Apr 2024 22:46:35 +0530 Subject: [PATCH 537/580] Add system chat check when rendering footer --- src/pages/home/report/ReportFooter.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index 1f352ed8a440..9a8ca2955127 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -83,6 +83,7 @@ function ReportFooter({ const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(report); const canWriteInReport = ReportUtils.canWriteInReport(report); + const isSystemChat = ReportUtils.isSystemChat(report); const allPersonalDetails = usePersonalDetails(); @@ -141,7 +142,7 @@ function ReportFooter({ /> )} {isArchivedRoom && <ArchivedReportFooter report={report} />} - {!isAnonymousUser && !canWriteInReport && <SystemChatReportFooterMessage />} + {!isAnonymousUser && !canWriteInReport && isSystemChat && <SystemChatReportFooterMessage />} {!isSmallScreenWidth && <View style={styles.offlineIndicatorRow}>{hideComposer && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}</View>} </View> )} From c7b6e355009af1d36f70d8b0e21f135f2858d32d Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Mon, 29 Apr 2024 17:42:20 +0000 Subject: [PATCH 538/580] Update version to 1.4.67-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b3e8170ca986..f557a2e3417e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046701 - versionName "1.4.67-1" + versionCode 1001046702 + versionName "1.4.67-2" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5f016311eff3..63f98c5e8972 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.67.1</string> + <string>1.4.67.2</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index d249f6d88c65..0435185421d3 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.67.1</string> + <string>1.4.67.2</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index b7d75cd6f03d..b8c877d10f26 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.67</string> <key>CFBundleVersion</key> - <string>1.4.67.1</string> + <string>1.4.67.2</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index b989563b8138..b5a6c7b2158b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.67-1", + "version": "1.4.67-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.67-1", + "version": "1.4.67-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1eed2fe08600..41394baa49b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.67-1", + "version": "1.4.67-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From aa13921f6fe8179618588582c3ad7e121ff04cb6 Mon Sep 17 00:00:00 2001 From: Rayane Djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 29 Apr 2024 19:51:07 +0100 Subject: [PATCH 539/580] adress review comments --- .github/workflows/failureNotifier.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/failureNotifier.yml b/.github/workflows/failureNotifier.yml index d10636041ac2..39dfbe8e84a7 100644 --- a/.github/workflows/failureNotifier.yml +++ b/.github/workflows/failureNotifier.yml @@ -38,12 +38,9 @@ jobs: repo: context.repo.repo, workflow_id: 'preDeploy.yml', }); - const run = allRuns.data.workflow_runs.find(run => run.id === runId); - const run_number = run.run_number; - const previousRun = allRuns.data.workflow_runs.find(run => run.run_number === run_number - 1); - if (previousRun.actor.login === 'OSBotify') { - return allRuns.data.workflow_runs.find(run => run.run_number === run_number - 2); - } + const filteredRuns = allRuns.data.workflow_runs.filter(run => run.actor.login !== 'OSBotify' && run.status !== 'cancelled'); + const currentIndex = filteredRuns.findIndex(run => run.id === runId); + const previousRun = filteredRuns[currentIndex + 1]; return previousRun; - name: Fetch Previous Workflow Run Jobs @@ -84,8 +81,7 @@ jobs: const jobName = jobs.jobs[i].name; const jobLink = jobs.jobs[i].html_url; const previousJob = previousRunJobs.jobs.find(job => job.name === jobName); - previousJobSucceeded = previousJob.conclusion === 'success'; - if (previousJobSucceeded) { + if (previousJob?.conclusion === 'success') { const annotations = await github.rest.checks.listAnnotations({ owner: context.repo.owner, repo: context.repo.repo, From 7bcb92ffb46536039e5452bc29606387363951da Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Mon, 29 Apr 2024 19:22:07 +0000 Subject: [PATCH 540/580] Update version to 1.4.67-3 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f557a2e3417e..13645e2bae54 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046702 - versionName "1.4.67-2" + versionCode 1001046703 + versionName "1.4.67-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 63f98c5e8972..7ed5f57c78f9 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.67.2</string> + <string>1.4.67.3</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0435185421d3..30663c0243ec 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.67.2</string> + <string>1.4.67.3</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index b8c877d10f26..883811e48d3a 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.67</string> <key>CFBundleVersion</key> - <string>1.4.67.2</string> + <string>1.4.67.3</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index b5a6c7b2158b..344dae2470d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.67-2", + "version": "1.4.67-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.67-2", + "version": "1.4.67-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 41394baa49b6..355082be790e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.67-2", + "version": "1.4.67-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From e6eb892e0ebe94ec63761906de3226fc3e52c059 Mon Sep 17 00:00:00 2001 From: Srikar Parsi <srikar.parsi@expensify.com> Date: Mon, 29 Apr 2024 15:29:01 -0400 Subject: [PATCH 541/580] Add 500ms timeout between RHP and training video --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index adc5f3517c11..b873170c971f 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -299,7 +299,7 @@ function FloatingActionButtonAndPopover( { icon: Expensicons.DocumentPlus, text: translate('iou.trackExpense'), - onSelected: () => { + onSelected: async () => { interceptAnonymousUser(() => IOU.startMoneyRequest( CONST.IOU.TYPE.TRACK, @@ -309,6 +309,7 @@ function FloatingActionButtonAndPopover( ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(), ), ); + await new Promise((resolve) => setTimeout(resolve, 500)); if (!hasSeenTrackTraining && !isOffline) { Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); } From cfbddf1396ad6002653a1cc495c479523c1a57f1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Tue, 30 Apr 2024 01:23:59 +0530 Subject: [PATCH 542/580] minor updates according to suggestions. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 8 ++++---- .../MoneyRequestPreviewContent.tsx | 12 +++++------- src/components/ReportActionItem/MoneyRequestView.tsx | 10 +++++----- src/languages/en.ts | 3 +-- src/languages/es.ts | 3 +-- src/libs/TransactionUtils.ts | 10 +++++----- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 73539656cd48..3d0435d41930 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -7,19 +7,19 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; -function ReceiptAuditHeader({notes = [], showAuditMessage = false}: {notes?: string[]; showAuditMessage?: boolean}) { +function ReceiptAuditHeader({notes = [], shouldShowAuditMessage = false}: {notes?: string[]; shouldShowAuditMessage?: boolean}) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const issuesFoundText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('common.verified'); + const auditText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('common.verified'); return ( <View style={[styles.ph5, styles.mbn1]}> <View style={[styles.flexRow, styles.alignItemsCenter]}> <Text style={[styles.textLabelSupporting]}>{translate('common.receipt')}</Text> - {showAuditMessage && ( + {shouldShowAuditMessage && ( <> - <Text style={[styles.textLabelSupporting]}>{` • ${issuesFoundText}`}</Text> + <Text style={[styles.textLabelSupporting]}>{` • ${auditText}`}</Text> <Icon width={12} height={12} diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index d53076fabd98..1ff34412bea7 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -89,18 +89,16 @@ function MoneyRequestPreviewContent({ const isSettlementOrApprovalPartial = Boolean(iouReport?.pendingFields?.partial); const isPartialHold = isSettlementOrApprovalPartial && isOnHold; const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '', transactionViolations); - const hasNoteTypeViolations = TransactionUtils.hasNoteTypeViolation(transaction?.transactionID ?? '', transactionViolations); + const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID ?? '', transactionViolations); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const isSettled = ReportUtils.isSettled(iouReport?.reportID); const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - const shouldShowRBR = - hasNoteTypeViolations || - hasViolations || - hasFieldErrors || - (!(isSettled && !isSettlementOrApprovalPartial) && !(ReportUtils.isReportApproved(iouReport) && !isSettlementOrApprovalPartial) && isOnHold); + const isFullySettled = isSettled && !isSettlementOrApprovalPartial; + const isFullyApproved = ReportUtils.isReportApproved(iouReport) && !isSettlementOrApprovalPartial; + const shouldShowRBR = hasNoticeTypeViolations || hasViolations || hasFieldErrors || (!isFullySettled && !isFullyApproved && isOnHold); /* Show the merchant for IOUs and expenses only if: @@ -182,7 +180,7 @@ function MoneyRequestPreviewContent({ } else if (!(isSettled && !isSettlementOrApprovalPartial) && isOnHold) { message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.hold')}`; } - } else if (hasNoteTypeViolations && transaction && !ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { + } else if (hasNoticeTypeViolations && transaction && !ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { message += ` • ${translate('violations.reviewRequired')}`; } else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID) && !isPartialHold) { message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.approved')}`; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d20b7be63092..4376ced21604 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -156,7 +156,7 @@ function MoneyRequestView({ const canEditReceipt = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); const hasReceipt = TransactionUtils.hasReceipt(transaction); const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); - const isReceiptScanCompleted = hasReceipt && TransactionUtils.isReceiptScanCompleted(transaction); + const didRceiptScanSucceed = hasReceipt && TransactionUtils.didRceiptScanSucceed(transaction); // TODO: remove the !isTrackExpense from this condition after this fix: https://github.com/Expensify/Expensify/issues/382786 const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE) && !isTrackExpense; @@ -187,7 +187,7 @@ function MoneyRequestView({ (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, [canUseViolations, getViolationsForField], ); - const noteTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noticeTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report); let amountDescription = `${translate('iou.amount')}`; @@ -333,8 +333,8 @@ function MoneyRequestView({ {shouldShowAnimatedBackground && <AnimatedEmptyStateBackground />} <View style={shouldShowAnimatedBackground && [StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth, true)]}> <ReceiptAuditHeader - notes={noteTypeViolations} - showAuditMessage={shouldShowNotesViolations && isReceiptScanCompleted} + notes={noticeTypeViolations} + shouldShowAuditMessage={shouldShowNotesViolations && didRceiptScanSucceed} /> {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {(showMapAsImage || hasReceipt) && ( @@ -386,7 +386,7 @@ function MoneyRequestView({ )} {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {!(!hasReceipt && (canEditReceipt || isAdmin || isApprover)) && !(showMapAsImage || hasReceipt) && <View style={{marginVertical: 6}} />} - {shouldShowNotesViolations && <ReceiptAuditMessages notes={noteTypeViolations} />} + {shouldShowNotesViolations && <ReceiptAuditMessages notes={noticeTypeViolations} />} <ViolationMessages violations={getViolationsForField('receipt')} /> <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription diff --git a/src/languages/en.ts b/src/languages/en.ts index 0b480341068c..aeed5ebec1c8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -297,7 +297,7 @@ export default { nonBillable: 'Non-billable', tag: 'Tag', receipt: 'Receipt', - verified: `Verified`, + verified: 'Verified', replace: 'Replace', distance: 'Distance', mile: 'mile', @@ -635,7 +635,6 @@ export default { posted: 'Posted', deleteReceipt: 'Delete receipt', receiptIssuesFound: (count: number) => `${count === 1 ? 'Issue' : 'Issues'} found`, - routePending: 'Pending...', fieldPending: 'Pending...', defaultRate: 'Default rate', receiptScanning: 'Scan in progress…', diff --git a/src/languages/es.ts b/src/languages/es.ts index 772aa3f9fa31..28acc79fa2f9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -287,7 +287,7 @@ export default { nonBillable: 'No facturable', tag: 'Etiqueta', receipt: 'Recibo', - verified: `Verificado`, + verified: 'Verificado', replace: 'Sustituir', distance: 'Distancia', mile: 'milla', @@ -628,7 +628,6 @@ export default { posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema' : 'Problemas'}`, - routePending: 'Pendiente...', fieldPending: 'Pendiente...', defaultRate: 'Tasa predeterminada', receiptScanning: 'Escaneo en curso…', diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 2ecb58dd4ba7..8059e6e3318c 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -491,7 +491,7 @@ function isReceiptBeingScanned(transaction: OnyxEntry<Transaction>): boolean { return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction?.receipt?.state); } -function isReceiptScanCompleted(transaction: OnyxEntry<Transaction>): boolean { +function didRceiptScanSucceed(transaction: OnyxEntry<Transaction>): boolean { return [CONST.IOU.RECEIPT_STATE.SCANCOMPLETE].some((value) => value === transaction?.receipt?.state); } @@ -600,9 +600,9 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti } /** - * Checks if any violations for the provided transaction are of type 'note' + * Checks if any violations for the provided transaction are of type 'notice' */ -function hasNoteTypeViolation(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): boolean { +function hasNoticeTypeViolation(transactionID: string, transactionViolations: OnyxCollection<TransactionViolation[]>): boolean { return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'notice')); } @@ -691,7 +691,7 @@ export { hasEReceipt, hasRoute, isReceiptBeingScanned, - isReceiptScanCompleted, + didRceiptScanSucceed, getValidWaypoints, isDistanceRequest, isFetchingWaypointsFromServer, @@ -711,7 +711,7 @@ export { waypointHasValidAddress, getRecentTransactions, hasViolation, - hasNoteTypeViolation, + hasNoticeTypeViolation, isCustomUnitRateIDForP2P, getRateID, }; From a0a2fc3ca95f77871813a88ec15ae5ab378912d7 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Tue, 30 Apr 2024 01:55:21 +0530 Subject: [PATCH 543/580] second commit: minor updates according to suggestions. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReceiptAudit.tsx | 2 +- src/components/ReportActionItem/MoneyRequestView.tsx | 4 ++-- src/languages/es.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 3d0435d41930..8ce9863e32ee 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -7,7 +7,7 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Text from './Text'; -function ReceiptAuditHeader({notes = [], shouldShowAuditMessage = false}: {notes?: string[]; shouldShowAuditMessage?: boolean}) { +function ReceiptAuditHeader({notes, shouldShowAuditMessage}: {notes: string[]; shouldShowAuditMessage: boolean}) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 4376ced21604..5494262680d5 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -187,7 +187,7 @@ function MoneyRequestView({ (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, [canUseViolations, getViolationsForField], ); - const noticeTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)); + const noticeTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)) ?? []; const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report); let amountDescription = `${translate('iou.amount')}`; @@ -334,7 +334,7 @@ function MoneyRequestView({ <View style={shouldShowAnimatedBackground && [StyleUtils.getReportWelcomeTopMarginStyle(isSmallScreenWidth, true)]}> <ReceiptAuditHeader notes={noticeTypeViolations} - shouldShowAuditMessage={shouldShowNotesViolations && didRceiptScanSucceed} + shouldShowAuditMessage={Boolean(shouldShowNotesViolations && didRceiptScanSucceed)} /> {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {(showMapAsImage || hasReceipt) && ( diff --git a/src/languages/es.ts b/src/languages/es.ts index 28acc79fa2f9..33c3f3d9aba3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -627,7 +627,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', - receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema' : 'Problemas'}`, + receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema' : 'Problemas'} encontrado`, fieldPending: 'Pendiente...', defaultRate: 'Tasa predeterminada', receiptScanning: 'Escaneo en curso…', From 9fbf78e82eeae5345d45889219dcde194bbf3842 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Tue, 30 Apr 2024 02:08:25 +0530 Subject: [PATCH 544/580] third commit: minor updates according to suggestions. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 5494262680d5..7a88ef7172e4 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -328,6 +328,9 @@ function MoneyRequestView({ </OfflineWithFeedback> ); + const shouldShowReceiptEmptyState = !hasReceipt && (canEditReceipt || isAdmin || isApprover); + const shouldShowMapOrReceipt = showMapAsImage ?? hasReceipt; + return ( <View style={[StyleUtils.getReportWelcomeContainerStyle(isSmallScreenWidth, true, shouldShowAnimatedBackground)]}> {shouldShowAnimatedBackground && <AnimatedEmptyStateBackground />} @@ -337,7 +340,7 @@ function MoneyRequestView({ shouldShowAuditMessage={Boolean(shouldShowNotesViolations && didRceiptScanSucceed)} /> {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} - {(showMapAsImage || hasReceipt) && ( + {shouldShowMapOrReceipt && ( <OfflineWithFeedback pendingAction={pendingAction} errors={transaction?.errors} @@ -367,7 +370,7 @@ function MoneyRequestView({ </View> </OfflineWithFeedback> )} - {!hasReceipt && (canEditReceipt || isAdmin || isApprover) && ( + {shouldShowReceiptEmptyState && ( <ReceiptEmptyState hasError={hasErrors} disabled={!canEditReceipt} @@ -385,7 +388,7 @@ function MoneyRequestView({ /> )} {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} - {!(!hasReceipt && (canEditReceipt || isAdmin || isApprover)) && !(showMapAsImage || hasReceipt) && <View style={{marginVertical: 6}} />} + {!shouldShowReceiptEmptyState && !shouldShowMapOrReceipt && <View style={{marginVertical: 6}} />} {shouldShowNotesViolations && <ReceiptAuditMessages notes={noticeTypeViolations} />} <ViolationMessages violations={getViolationsForField('receipt')} /> <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> From f45a75359b3ead739aa44581b255ba729f7313e1 Mon Sep 17 00:00:00 2001 From: Srikar Parsi <srikar.parsi@expensify.com> Date: Mon, 29 Apr 2024 17:03:02 -0400 Subject: [PATCH 545/580] refactor code --- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index b873170c971f..8ba49a1dfe9c 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -299,7 +299,7 @@ function FloatingActionButtonAndPopover( { icon: Expensicons.DocumentPlus, text: translate('iou.trackExpense'), - onSelected: async () => { + onSelected: () => { interceptAnonymousUser(() => IOU.startMoneyRequest( CONST.IOU.TYPE.TRACK, @@ -309,10 +309,11 @@ function FloatingActionButtonAndPopover( ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(), ), ); - await new Promise((resolve) => setTimeout(resolve, 500)); - if (!hasSeenTrackTraining && !isOffline) { - Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); - } + setTimeout(() => { + if (!hasSeenTrackTraining && !isOffline) { + Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); + } + }, 300); }, }, ] From b60404078c03a4ad49dec7d6aa739b31ad386011 Mon Sep 17 00:00:00 2001 From: Srikar Parsi <srikar.parsi@expensify.com> Date: Mon, 29 Apr 2024 17:04:07 -0400 Subject: [PATCH 546/580] use const --- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 8ba49a1dfe9c..af67c918108e 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -313,7 +313,7 @@ function FloatingActionButtonAndPopover( if (!hasSeenTrackTraining && !isOffline) { Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); } - }, 300); + }, CONST.ANIMATED_TRANSITION); }, }, ] From e117ef520000a22f6cb53d32369efd599601d768 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Tue, 30 Apr 2024 02:38:35 +0530 Subject: [PATCH 547/580] fourth commit: minor updates according to suggestions. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/ReportActionItem/MoneyRequestView.tsx | 3 ++- src/languages/es.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 01118fd766ba..36f5d7219dfa 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -334,7 +334,8 @@ function MoneyRequestView({ ); const shouldShowReceiptEmptyState = !hasReceipt && (canEditReceipt || isAdmin || isApprover); - const shouldShowMapOrReceipt = showMapAsImage ?? hasReceipt; + /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */ + const shouldShowMapOrReceipt = showMapAsImage || hasReceipt; return ( <View style={[StyleUtils.getReportWelcomeContainerStyle(isSmallScreenWidth, true, shouldShowAnimatedBackground)]}> diff --git a/src/languages/es.ts b/src/languages/es.ts index e30f5ecd8553..f132fe15027a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -629,7 +629,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', - receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema' : 'Problemas'} encontrado`, + receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`, fieldPending: 'Pendiente...', defaultRate: 'Tasa predeterminada', receiptScanning: 'Escaneo en curso…', From ce75ab8d1c18a98d2932269dd49f6bec10550178 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Mon, 29 Apr 2024 21:24:42 +0000 Subject: [PATCH 548/580] Update version to 1.4.67-4 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 13645e2bae54..7e475a80f68c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046703 - versionName "1.4.67-3" + versionCode 1001046704 + versionName "1.4.67-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7ed5f57c78f9..f5edd2dd2e2c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.67.3</string> + <string>1.4.67.4</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 30663c0243ec..d502fd28a08a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.67.3</string> + <string>1.4.67.4</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 883811e48d3a..23ab73168c89 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.67</string> <key>CFBundleVersion</key> - <string>1.4.67.3</string> + <string>1.4.67.4</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index 344dae2470d7..c3d039b55518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.67-3", + "version": "1.4.67-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.67-3", + "version": "1.4.67-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 355082be790e..db5296ad4b02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.67-3", + "version": "1.4.67-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 12c0936ff8e5cc7466d20c523880099810e9f85e Mon Sep 17 00:00:00 2001 From: Srikar Parsi <48188732+srikarparsi@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:47:57 -0400 Subject: [PATCH 549/580] lint --- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 5bc72c3e86b2..9429591b851f 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -331,11 +331,11 @@ function FloatingActionButtonAndPopover( ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(), ), ); - setTimeout(() => { - if (!hasSeenTrackTraining && !isOffline) { + if (!hasSeenTrackTraining && !isOffline) { + setTimeout(() => { Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL); - } - }, CONST.ANIMATED_TRANSITION); + }, CONST.ANIMATED_TRANSITION); + } }, }, ] From c88929d1220dadf2eedd44a6cc9d6b80d5a4824e Mon Sep 17 00:00:00 2001 From: Jack Nam <jack@expensify.com> Date: Mon, 29 Apr 2024 14:57:14 -0700 Subject: [PATCH 550/580] Re-add the missing changes in InitialURLContextProvider --- src/components/InitialURLContextProvider.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/InitialURLContextProvider.tsx b/src/components/InitialURLContextProvider.tsx index 710f045ede4e..a3df93844ca9 100644 --- a/src/components/InitialURLContextProvider.tsx +++ b/src/components/InitialURLContextProvider.tsx @@ -1,5 +1,6 @@ -import React, {createContext} from 'react'; +import React, {createContext, useEffect, useState} from 'react'; import type {ReactNode} from 'react'; +import {Linking} from 'react-native'; import type {Route} from '@src/ROUTES'; /** Initial url that will be opened when NewDot is embedded into Hybrid App. */ @@ -14,7 +15,16 @@ type InitialURLContextProviderProps = { }; function InitialURLContextProvider({children, url}: InitialURLContextProviderProps) { - return <InitialURLContext.Provider value={url}>{children}</InitialURLContext.Provider>; + const [initialURL, setInitialURL] = useState(url); + useEffect(() => { + if (initialURL) { + return; + } + Linking.getInitialURL().then((initURL) => { + setInitialURL(initURL as Route); + }); + }, [initialURL]); + return <InitialURLContext.Provider value={initialURL}>{children}</InitialURLContext.Provider>; } InitialURLContextProvider.displayName = 'InitialURLContextProvider'; From ee402ab397965a8d33cd15bc666876c6109012f5 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Mon, 29 Apr 2024 22:19:43 +0000 Subject: [PATCH 551/580] Update version to 1.4.67-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7e475a80f68c..d9e402067f83 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046704 - versionName "1.4.67-4" + versionCode 1001046705 + versionName "1.4.67-5" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f5edd2dd2e2c..0a76e4f49603 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.67.4</string> + <string>1.4.67.5</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index d502fd28a08a..0005b0698301 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.67.4</string> + <string>1.4.67.5</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 23ab73168c89..4df87d9d4bd4 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.67</string> <key>CFBundleVersion</key> - <string>1.4.67.4</string> + <string>1.4.67.5</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index c3d039b55518..ea5c97a6c81f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.67-4", + "version": "1.4.67-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.67-4", + "version": "1.4.67-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index db5296ad4b02..681fbcb54bd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.67-4", + "version": "1.4.67-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 7eb372d0a333d93b512e44517ad1c76aa2b2de3b Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Mon, 29 Apr 2024 22:34:47 +0000 Subject: [PATCH 552/580] Update version to 1.4.67-6 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index d9e402067f83..f0085205fe93 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046705 - versionName "1.4.67-5" + versionCode 1001046706 + versionName "1.4.67-6" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 0a76e4f49603..cbb84df9b366 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.67.5</string> + <string>1.4.67.6</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 0005b0698301..adb5d2b1f44f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.67.5</string> + <string>1.4.67.6</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 4df87d9d4bd4..119fa680680f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.67</string> <key>CFBundleVersion</key> - <string>1.4.67.5</string> + <string>1.4.67.6</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index ea5c97a6c81f..c7b61161b16b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.67-5", + "version": "1.4.67-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.67-5", + "version": "1.4.67-6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 681fbcb54bd6..fef340d3cb97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.67-5", + "version": "1.4.67-6", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 96bc6e85aa5e0526f47b4345bd77f6353ac38454 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Mon, 29 Apr 2024 22:36:53 +0000 Subject: [PATCH 553/580] Update version to 1.4.67-7 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index f0085205fe93..b4bdf6f6a760 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046706 - versionName "1.4.67-6" + versionCode 1001046707 + versionName "1.4.67-7" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index cbb84df9b366..4623ea965f63 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.67.6</string> + <string>1.4.67.7</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index adb5d2b1f44f..cde920aaf4a1 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.67.6</string> + <string>1.4.67.7</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 119fa680680f..f4ee74f39ba0 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.67</string> <key>CFBundleVersion</key> - <string>1.4.67.6</string> + <string>1.4.67.7</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index c7b61161b16b..344689a007a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.67-6", + "version": "1.4.67-7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.67-6", + "version": "1.4.67-7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index fef340d3cb97..10346ae3e941 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.67-6", + "version": "1.4.67-7", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 377121a978597e2a243cabd82349b1e8264161a2 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Mon, 29 Apr 2024 23:03:15 +0000 Subject: [PATCH 554/580] Update version to 1.4.68-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- ios/NotificationServiceExtension/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index b4bdf6f6a760..2b913f96c62f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046707 - versionName "1.4.67-7" + versionCode 1001046800 + versionName "1.4.68-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 4623ea965f63..7117078a1756 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> - <string>1.4.67</string> + <string>1.4.68</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleURLTypes</key> @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.67.7</string> + <string>1.4.68.0</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index cde920aaf4a1..42d3db845e0b 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ <key>CFBundlePackageType</key> <string>BNDL</string> <key>CFBundleShortVersionString</key> - <string>1.4.67</string> + <string>1.4.68</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.67.7</string> + <string>1.4.68.0</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index f4ee74f39ba0..ed783f6c5f7c 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ <key>CFBundleName</key> <string>$(PRODUCT_NAME)</string> <key>CFBundleShortVersionString</key> - <string>1.4.67</string> + <string>1.4.68</string> <key>CFBundleVersion</key> - <string>1.4.67.7</string> + <string>1.4.68.0</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index 344689a007a9..2246d24e9829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.67-7", + "version": "1.4.68-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.67-7", + "version": "1.4.68-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 10346ae3e941..7ef0fe36e1a5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.67-7", + "version": "1.4.68-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 22e6fcbb9a1a65897c9d2d40222a528fb6bb4cc4 Mon Sep 17 00:00:00 2001 From: Krishna Gupta <belivethatkg@gmail.com> Date: Tue, 30 Apr 2024 04:53:57 +0530 Subject: [PATCH 555/580] 5th commit: minor updates according to suggestions. Signed-off-by: Krishna Gupta <belivethatkg@gmail.com> --- src/components/MoneyRequestHeader.tsx | 2 +- src/components/ReceiptAudit.tsx | 2 +- .../ReportActionItem/MoneyRequestView.tsx | 15 ++++++--------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 5db2839654a0..6f5c54325d88 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -78,7 +78,7 @@ function MoneyRequestHeader({ // Only the requestor can take delete the expense, admins can only edit it. const isActionOwner = typeof parentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && parentReportAction.actorAccountID === session?.accountID; const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; + const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; const deleteTransaction = useCallback(() => { if (parentReportAction) { diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx index 8ce9863e32ee..ac1b36c6bf32 100644 --- a/src/components/ReceiptAudit.tsx +++ b/src/components/ReceiptAudit.tsx @@ -23,7 +23,7 @@ function ReceiptAuditHeader({notes, shouldShowAuditMessage}: {notes: string[]; s <Icon width={12} height={12} - src={notes.length > 0 ? Expensicons.DotIndicator : Expensicons.Checkmark} + src={notes.length ? Expensicons.DotIndicator : Expensicons.Checkmark} fill={notes.length ? theme.danger : theme.success} additionalStyles={styles.ml1} /> diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 36f5d7219dfa..a26c368ae170 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -128,7 +128,7 @@ function MoneyRequestView({ const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; - const hasPendingWaypoints = transaction?.pendingFields?.waypoints; + const hasPendingWaypoints = Boolean(transaction?.pendingFields?.waypoints); const showMapAsImage = isDistanceRequest && hasPendingWaypoints; const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); @@ -161,7 +161,7 @@ function MoneyRequestView({ const canEditDistance = ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE) && !isTrackExpense; const isAdmin = policy?.role === 'admin'; - const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && (session?.accountID ?? null) === moneyRequestReport?.managerID; + const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat const isPolicyExpenseChat = ReportUtils.isGroupPolicy(report); @@ -187,8 +187,6 @@ function MoneyRequestView({ (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, [canUseViolations, getViolationsForField], ); - const noticeTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)) ?? []; - const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report); let amountDescription = `${translate('iou.amount')}`; @@ -333,9 +331,10 @@ function MoneyRequestView({ </OfflineWithFeedback> ); - const shouldShowReceiptEmptyState = !hasReceipt && (canEditReceipt || isAdmin || isApprover); - /* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */ const shouldShowMapOrReceipt = showMapAsImage || hasReceipt; + const shouldShowReceiptEmptyState = !hasReceipt && (canEditReceipt || isAdmin || isApprover); + const noticeTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)) ?? []; + const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report); return ( <View style={[StyleUtils.getReportWelcomeContainerStyle(isSmallScreenWidth, true, shouldShowAnimatedBackground)]}> @@ -345,7 +344,6 @@ function MoneyRequestView({ notes={noticeTypeViolations} shouldShowAuditMessage={Boolean(shouldShowNotesViolations && didRceiptScanSucceed)} /> - {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {shouldShowMapOrReceipt && ( <OfflineWithFeedback pendingAction={pendingAction} @@ -393,10 +391,9 @@ function MoneyRequestView({ } /> )} - {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} {!shouldShowReceiptEmptyState && !shouldShowMapOrReceipt && <View style={{marginVertical: 6}} />} {shouldShowNotesViolations && <ReceiptAuditMessages notes={noticeTypeViolations} />} - <ViolationMessages violations={getViolationsForField('receipt')} /> + {canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />} <OfflineWithFeedback pendingAction={getPendingFieldAction('amount')}> <MenuItemWithTopDescription title={amountTitle} From 9415778b189fbe1035189173b6d7fa6bb7d0ab34 Mon Sep 17 00:00:00 2001 From: kmichel <kmichel1030@gmail.com> Date: Mon, 29 Apr 2024 16:33:11 -0700 Subject: [PATCH 556/580] fix type issue --- .../VideoPlayerContexts/VideoPopoverMenuContext.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx index b74a26caa628..29156c438d3d 100644 --- a/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx +++ b/src/components/VideoPlayerContexts/VideoPopoverMenuContext.tsx @@ -1,4 +1,3 @@ -import type {AVPlaybackSourceObject} from 'expo-av'; import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import type {PopoverMenuItem} from '@components/PopoverMenu'; @@ -34,7 +33,11 @@ function VideoPopoverMenuContextProvider({children}: ChildrenProps) { if (videoPopoverMenuPlayerRef.current === null) { return; } - const sourceURI = addEncryptedAuthTokenToURL((videoPopoverMenuPlayerRef.current.props.source as AVPlaybackSourceObject).uri); + const {source} = videoPopoverMenuPlayerRef.current?.props ?? {}; + if (typeof source === 'number' || !source) { + return; + } + const sourceURI = addEncryptedAuthTokenToURL(source.uri); fileDownload(sourceURI); }, [videoPopoverMenuPlayerRef]); From dab7b78dc65fc47f96054683bb62fa74c8375ffa Mon Sep 17 00:00:00 2001 From: Francois Laithier <francois@expensify.com> Date: Mon, 29 Apr 2024 16:34:22 -0700 Subject: [PATCH 557/580] Delete dupe MD file --- .../account-settings/Set-notifications.md | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 docs/articles/expensify-classic/settings/account-settings/Set-notifications.md diff --git a/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md b/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md deleted file mode 100644 index 2d561ea598d9..000000000000 --- a/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: Set notifications -description: Select your Expensify notification preferences ---- -<div id="expensify-classic" markdown="1"> - -{% include info.html %} -This process is currently not available from the mobile app and must be completed from the Expensify website. -{% include end-info.html %} - -1. Hover over Settings and click **Account**. -2. Click the **Preferences** tab on the left. -3. Scroll down to the Contact Preferences section. -4. Select the checkbox for the types of notifications you wish to receive. -</div> From dd2b879c46762f1d86a42645bd2b7a9a21ae362c Mon Sep 17 00:00:00 2001 From: Tsaqif <tsaiinkwa@yahoo.com> Date: Tue, 30 Apr 2024 06:35:59 +0700 Subject: [PATCH 558/580] Modify textInputLabel property name into searchInputLabel Signed-off-by: Tsaqif <tsaiinkwa@yahoo.com> --- src/components/CurrencySelectionList/index.tsx | 4 ++-- src/components/CurrencySelectionList/types.ts | 4 ++-- src/pages/iou/request/step/IOURequestStepCurrency.tsx | 2 +- src/pages/workspace/WorkspaceProfileCurrencyPage.tsx | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 70edaabeb551..361d82140326 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -8,7 +8,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CurrencyListItem, CurrencySelectionListOnyxProps, CurrencySelectionListProps} from './types'; -function CurrencySelectionList({textInputLabel, initiallySelectedCurrencyCode, onSelect, currencyList}: CurrencySelectionListProps) { +function CurrencySelectionList({searchInputLabel, initiallySelectedCurrencyCode, onSelect, currencyList}: CurrencySelectionListProps) { const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); @@ -44,7 +44,7 @@ function CurrencySelectionList({textInputLabel, initiallySelectedCurrencyCode, o <SelectionList sections={sections} ListItem={RadioListItem} - textInputLabel={textInputLabel} + textInputLabel={searchInputLabel} textInputValue={searchValue} onChangeText={setSearchValue} onSelectRow={onSelect} diff --git a/src/components/CurrencySelectionList/types.ts b/src/components/CurrencySelectionList/types.ts index a8b8cc38fd90..eb7cf72d4e1e 100644 --- a/src/components/CurrencySelectionList/types.ts +++ b/src/components/CurrencySelectionList/types.ts @@ -8,13 +8,13 @@ type CurrencyListItem = ListItem & { }; type CurrencySelectionListOnyxProps = { - /** Constant, list of available currencies */ + /** List of available currencies */ currencyList: OnyxEntry<CurrencyList>; }; type CurrencySelectionListProps = CurrencySelectionListOnyxProps & { /** Label for the search text input */ - textInputLabel: string; + searchInputLabel: string; /** Currency item to be selected initially */ initiallySelectedCurrencyCode?: string; diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.tsx b/src/pages/iou/request/step/IOURequestStepCurrency.tsx index 12e47c4219c7..8669563f3b9f 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.tsx +++ b/src/pages/iou/request/step/IOURequestStepCurrency.tsx @@ -74,7 +74,7 @@ function IOURequestStepCurrency({ > {({didScreenTransitionEnd}) => ( <CurrencySelectionList - textInputLabel={translate('common.search')} + searchInputLabel={translate('common.search')} onSelect={(option: CurrencyListItem) => { if (!didScreenTransitionEnd) { return; diff --git a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx index 4940bceee0e1..85da1edfcab6 100644 --- a/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceProfileCurrencyPage.tsx @@ -12,9 +12,9 @@ import AdminPolicyAccessOrNotFoundWrapper from './AdminPolicyAccessOrNotFoundWra import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -type WorkspaceProfileCurrentPageProps = WithPolicyAndFullscreenLoadingProps; +type WorkspaceProfileCurrencyPageProps = WithPolicyAndFullscreenLoadingProps; -function WorkspaceProfileCurrencyPage({policy}: WorkspaceProfileCurrentPageProps) { +function WorkspaceProfileCurrencyPage({policy}: WorkspaceProfileCurrencyPageProps) { const {translate} = useLocalize(); const onSelectCurrency = (item: CurrencyListItem) => { @@ -38,7 +38,7 @@ function WorkspaceProfileCurrencyPage({policy}: WorkspaceProfileCurrentPageProps /> <CurrencySelectionList - textInputLabel={translate('workspace.editor.currencyInputLabel')} + searchInputLabel={translate('workspace.editor.currencyInputLabel')} onSelect={onSelectCurrency} initiallySelectedCurrencyCode={policy?.outputCurrency} /> From 504eec7ea643b8e5794c1b3650a816901b4bed69 Mon Sep 17 00:00:00 2001 From: Aldo Canepa <aldo@expensify.com> Date: Mon, 29 Apr 2024 16:42:04 -0700 Subject: [PATCH 559/580] Update export date translations --- src/CONST.ts | 6 ++--- src/languages/en.ts | 23 +++++++++++++++---- src/languages/es.ts | 23 +++++++++++++++---- .../QuickbooksExportConfigurationPage.tsx | 2 +- .../export/QuickbooksExportDateSelectPage.tsx | 8 +++---- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 7c14aef7bebf..676faf2ba6e9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4698,9 +4698,9 @@ const CONST = { }, QUICKBOOKS_EXPORT_DATE: { - LAST_EXPENSE: 'lastExpense', - EXPORTED_DATE: 'exportedDate', - SUBMITTED_DATA: 'submittedData', + LAST_EXPENSE: 'LAST_EXPENSE', + REPORT_EXPORTED: 'REPORT_EXPORTED', + REPORT_SUBMITTED: 'REPORT_SUBMITTED', }, QUICKBOOKS_EXPORT_COMPANY_CARD: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 9062fb526ed1..1af3c35c268d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1911,11 +1911,24 @@ export default { exportExpensifyCard: 'Export Expensify Card transactions as', deepDiveExpensifyCard: 'Expensify Card transactions automatically export to a "Expensify Card Liability Account" created with', deepDiveExpensifyCardIntegration: 'our integration.', - exportDate: 'Export date', - exportDateDescription: 'Use this date when exporting reports to QuickBooks Online.', - lastExpense: {label: 'Date of last expense', description: 'The date of the most recent expense on the report'}, - exportedDate: {label: 'Export date', description: 'The date the report was exported to QuickBooks Online'}, - submittedData: {label: 'Submitted date', description: 'The date the report was submitted for approval'}, + exportDate: { + label: 'Export date', + description: 'Use this date when exporting reports to QuickBooks Online.', + values: { + [CONST.QUICKBOOKS_EXPORT_DATE.LAST_EXPENSE]: { + label: 'Date of last expense', + description: 'The date of the most recent expense on the report', + }, + [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_EXPORTED]: { + label: 'Export date', + description: 'The date the report was exported to QuickBooks Online', + }, + [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_SUBMITTED]: { + label: 'Submitted date', + description: 'The date the report was submitted for approval', + }, + }, + }, receivable: 'Accounts receivable', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. archive: 'Accounts receivable archive', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. exportInvoicesDescription: 'Invoices will be exported to this account in QuickBooks Online.', diff --git a/src/languages/es.ts b/src/languages/es.ts index a14c71e23c0c..15badf3c8791 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1935,11 +1935,24 @@ export default { deepDiveExpensifyCard: 'Las transacciones de la Tarjeta Expensify se exportan automáticamente a una "Cuenta de Responsabilidad de la Tarjeta Expensify" creada con', deepDiveExpensifyCardIntegration: 'nuestra integración.', exportExpensifyCard: 'Exportar las transacciones de las tarjetas Expensify como', - exportDate: 'Fecha de exportación', - exportDateDescription: 'Use this date when exporting reports to QuickBooks Online.', - lastExpense: {label: 'Date of last expense', description: 'The date of the most recent expense on the report'}, - exportedDate: {label: 'Fecha de exportación', description: 'Fecha de exportación del informe a QuickBooks Online'}, - submittedData: {label: 'Fecha de envío', description: 'Fecha en la que el informe se envió para su aprobación'}, + exportDate: { + label: 'Fecha de exportación', + description: 'Use this date when exporting reports to QuickBooks Online.', + values: { + [CONST.QUICKBOOKS_EXPORT_DATE.LAST_EXPENSE]: { + label: 'Date of last expense', + description: 'The date of the most recent expense on the report', + }, + [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_EXPORTED]: { + label: 'Fecha de exportación', + description: 'Fecha de exportación del informe a QuickBooks Online', + }, + [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_SUBMITTED]: { + label: 'Fecha de envío', + description: 'Fecha en la que el informe se envió para su aprobación', + }, + }, + }, receivable: 'Cuentas por cobrar', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. archive: 'Archivo de cuentas por cobrar', // This is an account name that will come directly from QBO, so I don't know why we need a translation for it. It should take whatever the name of the account is in QBO. Leaving this note for CS. exportInvoicesDescription: 'Las facturas se exportarán a esta cuenta en QuickBooks Online.', diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx index 4d550d11547f..a7e3916441f3 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportConfigurationPage.tsx @@ -39,7 +39,7 @@ function QuickbooksExportConfigurationPage({policy}: WithPolicyConnectionsProps) description: translate('workspace.qbo.date'), onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT_DATE_SELECT.getRoute(policyID)), brickRoadIndicator: errorFields?.exportDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - title: exportDate ? translate(`workspace.qbo.${exportDate}.label`) : undefined, + title: exportDate ? translate(`workspace.qbo.exportDate.values.${exportDate}.label`) : undefined, pendingAction: pendingFields?.exportDate, error: errorFields?.exportDate ? translate('common.genericErrorMessage') : undefined, }, diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx index e45014a7f70d..16e9a20668c5 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksExportDateSelectPage.tsx @@ -26,8 +26,8 @@ function QuickbooksExportDateSelectPage({policy}: WithPolicyConnectionsProps) { const {exportDate} = policy?.connections?.quickbooksOnline?.config ?? {}; const data: CardListItem[] = Object.values(CONST.QUICKBOOKS_EXPORT_DATE).map((dateType) => ({ value: dateType, - text: translate(`workspace.qbo.${dateType}.label`), - alternateText: translate(`workspace.qbo.${dateType}.description`), + text: translate(`workspace.qbo.exportDate.values.${dateType}.label`), + alternateText: translate(`workspace.qbo.exportDate.values.${dateType}.description`), keyForList: dateType, isSelected: exportDate === dateType, })); @@ -52,9 +52,9 @@ function QuickbooksExportDateSelectPage({policy}: WithPolicyConnectionsProps) { includeSafeAreaPaddingBottom={false} testID={QuickbooksExportDateSelectPage.displayName} > - <HeaderWithBackButton title={translate('workspace.qbo.exportDate')} /> + <HeaderWithBackButton title={translate('workspace.qbo.exportDate.label')} /> <SelectionList - headerContent={<Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.exportDateDescription')}</Text>} + headerContent={<Text style={[styles.ph5, styles.pb5]}>{translate('workspace.qbo.exportDate.description')}</Text>} sections={[{data}]} ListItem={RadioListItem} onSelectRow={selectExportDate} From dd9d0008f94d1702387009c5375b5f1c183cfcff Mon Sep 17 00:00:00 2001 From: Aldo Canepa <aldo@expensify.com> Date: Mon, 29 Apr 2024 16:47:20 -0700 Subject: [PATCH 560/580] Improve spanish translations --- src/languages/es.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 15badf3c8791..c45ccb9e0123 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1937,11 +1937,11 @@ export default { exportExpensifyCard: 'Exportar las transacciones de las tarjetas Expensify como', exportDate: { label: 'Fecha de exportación', - description: 'Use this date when exporting reports to QuickBooks Online.', + description: 'Usar esta fecha al exportar informe a QuickBooks Online.', values: { [CONST.QUICKBOOKS_EXPORT_DATE.LAST_EXPENSE]: { - label: 'Date of last expense', - description: 'The date of the most recent expense on the report', + label: 'Fecha del último gasto', + description: 'Fecha del gasto mas reciente en el informe', }, [CONST.QUICKBOOKS_EXPORT_DATE.REPORT_EXPORTED]: { label: 'Fecha de exportación', From 087f3dd8ab650b2e3429ff5d92affd22bf5ce0f6 Mon Sep 17 00:00:00 2001 From: Aldo Canepa Garay <87341702+aldo-expensify@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:35:21 -0700 Subject: [PATCH 561/580] Fix comment --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index c45ccb9e0123..60412cd2a3e3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1937,7 +1937,7 @@ export default { exportExpensifyCard: 'Exportar las transacciones de las tarjetas Expensify como', exportDate: { label: 'Fecha de exportación', - description: 'Usar esta fecha al exportar informe a QuickBooks Online.', + description: 'Usa esta fecha al exportar informe a QuickBooks Online.', values: { [CONST.QUICKBOOKS_EXPORT_DATE.LAST_EXPENSE]: { label: 'Fecha del último gasto', From 3f6427fd5ccf9c3b49cd480e8598b964ce85937a Mon Sep 17 00:00:00 2001 From: Hayata Suenaga <hayata@expensify.com> Date: Mon, 29 Apr 2024 18:32:09 -0700 Subject: [PATCH 562/580] feat: add custom hook to get policy with connections field --- src/hooks/usePolicyWithConnections.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/hooks/usePolicyWithConnections.ts diff --git a/src/hooks/usePolicyWithConnections.ts b/src/hooks/usePolicyWithConnections.ts new file mode 100644 index 000000000000..51aac98d940e --- /dev/null +++ b/src/hooks/usePolicyWithConnections.ts @@ -0,0 +1,25 @@ +import {useEffect} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import {openPolicyAccountingPage} from '@libs/actions/PolicyConnections'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useNetwork from './useNetwork'; + +export default function usePolicyConnections(policyID: string) { + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const {isOffline} = useNetwork(); + const [hasConnectionsDataBeenFetched] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_HAS_CONNECTIONS_DATA_BEEN_FETCHED}${policy?.id ?? '0'}`, { + initWithStoredValues: false, + }); + + useEffect(() => { + // When the accounting feature is not enabled, or if the connections data already exists, + // there is no need to fetch the connections data. + if (isOffline || !policy || !policy.areConnectionsEnabled || !!hasConnectionsDataBeenFetched || !!policy.connections) { + return; + } + + openPolicyAccountingPage(policy.id); + }, [hasConnectionsDataBeenFetched, policy, isOffline]); + + return policy; +} From 6ff87da8c227cc0ebe35a4d221fbf5cb8d04e610 Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Tue, 30 Apr 2024 08:23:35 +0200 Subject: [PATCH 563/580] add small fixes to TransactionListItem after review --- src/components/Search.tsx | 6 ++--- .../SelectionList/TransactionListItem.tsx | 24 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index fd2232a29fcd..ac2b54a4fb0d 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -27,7 +27,7 @@ const mockData = [ amount: 12500, type: 'cash', reportID: '3632789879960357', - transactionThreadReportID: '2', + transactionThreadReportID: '3632789879960357', transactionID: '1234', modifiedCreated: '2024-05-06 00:00:00', description: 'description description description description', @@ -45,7 +45,7 @@ const mockData = [ amount: 12500, type: 'card', // not present in live data (data outside of snapshot_) reportID: '5768873634031661', - transactionThreadReportID: '2', + transactionThreadReportID: '3632789879960357', transactionID: '5555', modifiedCreated: '2024-05-06 00:00:00', description: 'description', @@ -129,7 +129,7 @@ function Search({query}: SearchProps) { ListItem={ListItem} sections={[{data: mockData, isDisabled: false}]} onSelectRow={(item) => { - openReport(item.reportID); + openReport(item.transactionThreadReportID); }} onSelectAll={!isSmallScreenWidth ? () => {} : undefined} onCheckboxPress={() => {}} diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 8981cf87ec2a..d0eabcedd2be 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -142,15 +142,15 @@ function TransactionListItem<TItem extends ListItem>({ <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.managerID]?.avatar} - name={personalDetails[item.managerID]?.displayName} + source={personalDetails[item.accountID]?.avatar} + name={personalDetails[item.accountID]?.displayName} type={CONST.ICON_TYPE_WORKSPACE} /> <Text numberOfLines={1} style={[styles.flex1, styles.flexGrow1, styles.textStrong, styles.textMicro]} > - {personalDetails[item.managerID]?.displayName} + {personalDetails[item.accountID]?.displayName} </Text> </View> <Icon @@ -163,15 +163,15 @@ function TransactionListItem<TItem extends ListItem>({ <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.accountID]?.avatar} - name={personalDetails[item.accountID]?.displayName} + source={personalDetails[item.managerID]?.avatar} + name={personalDetails[item.managerID]?.displayName} type={CONST.ICON_TYPE_WORKSPACE} /> <Text numberOfLines={1} style={[styles.flex1, styles.flexGrow1, styles.textStrong, styles.textMicro]} > - {personalDetails[item.accountID]?.displayName} + {personalDetails[item.managerID]?.displayName} </Text> </View> </View> @@ -251,15 +251,15 @@ function TransactionListItem<TItem extends ListItem>({ <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.managerID]?.avatar} - name={personalDetails[item.managerID]?.displayName} + source={personalDetails[item.accountID]?.avatar} + name={personalDetails[item.accountID]?.displayName} type={CONST.ICON_TYPE_WORKSPACE} /> <Text numberOfLines={1} style={[styles.flex1, styles.flexGrow1, styles.textStrong]} > - {personalDetails[item.managerID]?.displayName} + {personalDetails[item.accountID]?.displayName} </Text> </View> </View> @@ -268,15 +268,15 @@ function TransactionListItem<TItem extends ListItem>({ <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.accountID]?.avatar} - name={personalDetails[item.accountID]?.displayName} + source={personalDetails[item.managerID]?.avatar} + name={personalDetails[item.managerID]?.displayName} type={CONST.ICON_TYPE_WORKSPACE} /> <Text numberOfLines={1} style={[styles.flex1, styles.flexGrow1, styles.textStrong]} > - {personalDetails[item.accountID]?.displayName} + {personalDetails[item.managerID]?.displayName} </Text> </View> </View> From 516c46651fed9fd587e761193aaa46d39b290c36 Mon Sep 17 00:00:00 2001 From: Robert Chen <robert@expensify.com> Date: Tue, 30 Apr 2024 15:22:51 +0800 Subject: [PATCH 564/580] Revert "[CP Staging] Chat is not scrolled to the new message on a newly created WS chat after whisper message" --- src/hooks/useReportScrollManager/index.ts | 12 ++++-------- src/pages/home/report/ReportActionsList.tsx | 10 ++++------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/hooks/useReportScrollManager/index.ts b/src/hooks/useReportScrollManager/index.ts index 0d52dfd63159..8b56cd639d08 100644 --- a/src/hooks/useReportScrollManager/index.ts +++ b/src/hooks/useReportScrollManager/index.ts @@ -21,15 +21,11 @@ function useReportScrollManager(): ReportScrollManagerData { * Scroll to the bottom of the flatlist. */ const scrollToBottom = useCallback(() => { - // We're deferring execution here because on iOS: mWeb (WebKit based browsers) - // scrollToOffset method doesn't work unless called on the next tick - requestAnimationFrame(() => { - if (!flatListRef?.current) { - return; - } + if (!flatListRef?.current) { + return; + } - flatListRef.current.scrollToOffset({animated: false, offset: 0}); - }); + flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, [flatListRef]); return {ref: flatListRef, scrollToIndex, scrollToBottom}; diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 3c6038697c67..c280a093cb13 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -325,16 +325,14 @@ function ReportActionsList({ const scrollToBottomForCurrentUserAction = useCallback( (isFromCurrentUser: boolean) => { - // If a new comment is added and it's from the current user scroll to the bottom - // otherwise leave the user positioned where they are now in the list. - // Additionally, since the first report action could be a whisper message (new WS) -> - // hasNewestReportAction will be false, check isWhisperAction is false before returning early. - if (!isFromCurrentUser || (!hasNewestReportActionRef.current && !ReportActionsUtils.isWhisperAction(sortedReportActions?.[0]))) { + // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where + // they are now in the list. + if (!isFromCurrentUser || !hasNewestReportActionRef.current) { return; } InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); }, - [sortedReportActions, reportScrollManager], + [reportScrollManager], ); useEffect(() => { // Why are we doing this, when in the cleanup of the useEffect we are already calling the unsubscribe function? From 8133745d6e8b1233e6743ac8a43c4af35517cf82 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:00:13 +0530 Subject: [PATCH 565/580] Update en.ts --- src/languages/en.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index e3e9c90d2aaf..53645d19fe8f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -705,7 +705,9 @@ export default { other: 'Unexpected error, please try again later.', genericCreateFailureMessage: 'Unexpected error submitting this expense. Please try again later.', genericCreateInvoiceFailureMessage: 'Unexpected error sending invoice, please try again later.', + // eslint-disable-next-line use-periods-for-error-messages receiptFailureMessage: "The receipt didn't upload. ", + // eslint-disable-next-line use-periods-for-error-messages saveFileMessage: 'Download the file ', loseFileMessage: 'or dismiss this error and lose it.', genericDeleteFailureMessage: 'Unexpected error deleting this expense, please try again later.', From 71d1b58f0e6b47aaa2e707be7f4c2382b60ea023 Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Tue, 30 Apr 2024 09:45:28 +0200 Subject: [PATCH 566/580] add using actual api results --- src/components/Search.tsx | 81 ++++++++----------- .../SelectionList/TransactionListItem.tsx | 4 +- src/libs/actions/Search.ts | 14 ---- 3 files changed, 36 insertions(+), 63 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index ac2b54a4fb0d..4269cf812f36 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -19,44 +19,21 @@ import SelectionList from './SelectionList'; import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; import Text from './Text'; -const mockData = [ - { - receipt: {source: 'http...'}, - hasEReceipt: false, - created: '2024-04-11 00:00:00', - amount: 12500, - type: 'cash', - reportID: '3632789879960357', - transactionThreadReportID: '3632789879960357', - transactionID: '1234', - modifiedCreated: '2024-05-06 00:00:00', - description: 'description description description description', - accountID: '8392101', - managerID: '8392101', - currency: 'USD', - modifiedCurrency: '', - category: 'Bananas', - tag: 'Green', - }, - { - receipt: {source: 'http...'}, - hasEReceipt: false, - created: '2024-04-11 00:00:00', - amount: 12500, - type: 'card', // not present in live data (data outside of snapshot_) - reportID: '5768873634031661', - transactionThreadReportID: '3632789879960357', - transactionID: '5555', - modifiedCreated: '2024-05-06 00:00:00', - description: 'description', - accountID: '8392101', - managerID: '8392101', - currency: 'USD', - modifiedCurrency: '', - category: 'Bananas', - tag: 'Green', - }, -]; +/** + * Todo This is a temporary function that will pick search results from under `snapshot_` key + * either api needs to be updated to key by `snapshot_hash` or app code calling search data needs to be refactored + * remove this function once this is properly fixed + */ +function getCleanSearchResults(searchResults: unknown) { + if (!searchResults) { + return {}; + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // eslint-disable-next-line no-underscore-dangle,@typescript-eslint/no-unsafe-return + return searchResults.snapshot_?.data; +} type SearchProps = { query: string; @@ -70,16 +47,23 @@ function Search({query}: SearchProps) { // const [selectedCategories, setSelectedCategories] = useState<Record<string, boolean>>({}); useCustomBackHandler(); - const hash = SearchUtils.getQueryHash(query); - const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); + // Todo bring back hash when api is updated + // const hash = SearchUtils.getQueryHash(query); + // const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); + const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}`); useEffect(() => { + if (isOffline) { + return; + } + SearchActions.search(query); - }, [query]); + }, [query, isOffline]); + + const cleanResults = getCleanSearchResults(searchResults); - const isLoading = !isOffline && isLoadingOnyxValue(searchResultsMeta); - // Todo remove using mock data once api is done - const shouldShowEmptyState = !isEmptyObject(searchResults) || !mockData; + const isLoading = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || cleanResults === undefined; + const shouldShowEmptyState = !isLoading && isEmptyObject(cleanResults); if (isLoading) { return <TableListItemSkeleton shouldAnimate />; @@ -115,19 +99,22 @@ function Search({query}: SearchProps) { }; const openReport = (reportID?: string) => { - if (reportID) { - Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(query, reportID)); + if (!reportID) { + return; } + + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(query, reportID)); }; const ListItem = SearchUtils.getListItem(); + const data = SearchUtils.getSections(cleanResults ?? {}); return ( <SelectionList canSelectMultiple customListHeader={getListHeader()} ListItem={ListItem} - sections={[{data: mockData, isDisabled: false}]} + sections={[{data, isDisabled: false}]} onSelectRow={(item) => { openReport(item.transactionThreadReportID); }} diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index d0eabcedd2be..938c53879de5 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -50,7 +50,7 @@ function TransactionListItem<TItem extends ListItem>({ const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); - const {isMediumScreenWidth, isSmallScreen} = useWindowDimensions(); + const {isSmallScreenWidth} = useWindowDimensions(); const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; @@ -110,7 +110,7 @@ function TransactionListItem<TItem extends ListItem>({ /> ); - const displayNarrowVersion = isMediumScreenWidth || isSmallScreen; + const displayNarrowVersion = isSmallScreenWidth; if (displayNarrowVersion) { return ( diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 4bb78d7c161d..03179fae93cc 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1,22 +1,8 @@ -import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import {READ_COMMANDS} from '@libs/API/types'; import * as SearchUtils from '@libs/SearchUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; - -let isNetworkOffline = false; -Onyx.connect({ - key: ONYXKEYS.NETWORK, - callback: (value) => { - isNetworkOffline = value?.isOffline ?? false; - }, -}); function search(query: string) { - if (isNetworkOffline) { - return; - } - const hash = SearchUtils.getQueryHash(query); API.read(READ_COMMANDS.SEARCH, {query, hash}); } From d4de57d48896e9ec6e42b2625788d27984c87004 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:20:11 +0530 Subject: [PATCH 567/580] Update en.ts --- src/languages/en.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 53645d19fe8f..ea7777888b1a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -705,9 +705,9 @@ export default { other: 'Unexpected error, please try again later.', genericCreateFailureMessage: 'Unexpected error submitting this expense. Please try again later.', genericCreateInvoiceFailureMessage: 'Unexpected error sending invoice, please try again later.', - // eslint-disable-next-line use-periods-for-error-messages + // eslint-disable-next-line rulesdir/use-periods-for-error-messages receiptFailureMessage: "The receipt didn't upload. ", - // eslint-disable-next-line use-periods-for-error-messages + // eslint-disable-next-line rulesdir/use-periods-for-error-messages saveFileMessage: 'Download the file ', loseFileMessage: 'or dismiss this error and lose it.', genericDeleteFailureMessage: 'Unexpected error deleting this expense, please try again later.', From 748a5298b61ef739ba49bacde29def46eb4d7c53 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:30:25 +0530 Subject: [PATCH 568/580] Fix lint --- src/languages/es.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index 31cc65c1fce6..4b84f3d86234 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -700,7 +700,9 @@ export default { other: 'Error inesperado, por favor inténtalo más tarde.', genericCreateFailureMessage: 'Error inesperado al enviar este gasto. Por favor, inténtalo más tarde.', genericCreateInvoiceFailureMessage: 'Error inesperado al enviar la factura, inténtalo de nuevo más tarde.', + // eslint-disable-next-line rulesdir/use-periods-for-error-messages receiptFailureMessage: 'El recibo no se subió. ', + // eslint-disable-next-line rulesdir/use-periods-for-error-messages saveFileMessage: 'Guarda el archivo ', loseFileMessage: 'o descarta este error y piérdelo.', genericDeleteFailureMessage: 'Error inesperado al eliminar este gasto. Por favor, inténtalo más tarde.', From 29d2fa539e62022c9585e4acaa50a9ca8a9e0204 Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Tue, 30 Apr 2024 10:13:35 +0200 Subject: [PATCH 569/580] Handle opening search page with invalid queries --- src/CONST.ts | 9 ++++---- src/pages/Search/SearchPage.tsx | 29 ++++++++++++++++++------ src/pages/Search/SearchPageBottomTab.tsx | 28 +++++++++++++++++------ src/types/onyx/SearchResults.ts | 4 +++- 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index af0499d7f663..2b77a5f1c178 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3400,10 +3400,11 @@ const CONST = { }, TAB_SEARCH: { ALL: 'all', - SHARED: 'shared', - DRAFTS: 'drafts', - WAITING_ON_YOU: 'waitingOnYou', - FINISHED: 'finished', + // @TODO: Uncomment when the queries below are implemented + // SHARED: 'shared', + // DRAFTS: 'drafts', + // WAITING_ON_YOU: 'waitingOnYou', + // FINISHED: 'finished', }, STATUS_TEXT_MAX_LENGTH: 100, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 619d1eee05d7..777ccaad4e9d 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -1,26 +1,41 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; +import Navigation from '@libs/Navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import type {SearchQuery} from '@src/types/onyx/SearchResults'; type SearchPageProps = StackScreenProps<CentralPaneNavigatorParamList, typeof SCREENS.SEARCH.CENTRAL_PANE>; function SearchPage({route}: SearchPageProps) { const currentQuery = route?.params && 'query' in route.params ? route?.params?.query : ''; - const query = String(currentQuery); + const query = currentQuery as SearchQuery; + const isValidQuery = Object.values(CONST.TAB_SEARCH).includes(query); + + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL)); return ( <ScreenWrapper testID={Search.displayName}> - <HeaderWithBackButton - title="All" - icon={Illustrations.MoneyReceipts} - shouldShowBackButton={false} - /> - <Search query={query} />; + <FullPageNotFoundView + shouldForceFullScreen + shouldShow={!isValidQuery} + onBackButtonPress={handleOnBackButtonPress} + shouldShowLink={false} + > + <HeaderWithBackButton + title="All" + icon={Illustrations.MoneyReceipts} + shouldShowBackButton={false} + /> + <Search query={query} /> + </FullPageNotFoundView> </ScreenWrapper> ); } diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 169485f44001..59e59b456724 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -1,11 +1,16 @@ import React from 'react'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ScreenWrapper from '@components/ScreenWrapper'; import Search from '@components/Search'; import useActiveRoute from '@hooks/useActiveRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; import TopBar from '@navigation/AppNavigator/createCustomBottomTabNavigator/TopBar'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {SearchQuery} from '@src/types/onyx/SearchResults'; import SearchFilters from './SearchFilters'; function SearchPageBottomTab() { @@ -15,19 +20,28 @@ function SearchPageBottomTab() { const styles = useThemeStyles(); const currentQuery = activeRoute?.params && 'query' in activeRoute.params ? activeRoute?.params?.query : ''; - const query = String(currentQuery); + const query = currentQuery as SearchQuery; + const isValidQuery = Object.values(CONST.TAB_SEARCH).includes(query); + + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH.getRoute(CONST.TAB_SEARCH.ALL)); return ( <ScreenWrapper testID={SearchPageBottomTab.displayName} style={styles.pt0} > - <TopBar - breadcrumbLabel={translate('common.search')} - shouldDisplaySearch={false} - /> - <SearchFilters query={query} /> - {isSmallScreenWidth && <Search query={query} />} + <FullPageNotFoundView + shouldShow={!isValidQuery} + onBackButtonPress={handleOnBackButtonPress} + shouldShowLink={false} + > + <TopBar + breadcrumbLabel={translate('common.search')} + shouldDisplaySearch={false} + /> + <SearchFilters query={query} /> + {isSmallScreenWidth && <Search query={query} />} + </FullPageNotFoundView> </ScreenWrapper> ); } diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 4b1127943cae..f86f688f7819 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -34,6 +34,8 @@ type SearchTransaction = { type SearchTransactionType = ValueOf<typeof CONST.SEARCH_TRANSACTION_TYPE>; +type SearchQuery = ValueOf<typeof CONST.TAB_SEARCH>; + type SearchResults = { search: SearchResultsInfo; data: Record<string, SearchTransaction>; @@ -41,4 +43,4 @@ type SearchResults = { export default SearchResults; -export type {SearchTransaction, SearchTransactionType}; +export type {SearchTransaction, SearchTransactionType, SearchQuery}; From 9114d95f4261594679f40a58ea653e5067121f24 Mon Sep 17 00:00:00 2001 From: war-in <war-in@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:49:31 +0200 Subject: [PATCH 570/580] remove sending <mention-report> from room/workspace descriptions --- src/libs/ReportUtils.ts | 14 ++++++++------ src/libs/actions/Policy.ts | 2 +- src/pages/iou/HoldReasonPage.tsx | 2 +- src/pages/workspace/WorkspaceNewRoomPage.tsx | 6 +++--- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 257e1a4478f6..7141cad8c245 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -477,7 +477,6 @@ type OutstandingChildRequest = { type ParsingDetails = { shouldEscapeText?: boolean; reportID?: string; - policyID?: string; }; let currentUserEmail: string | undefined; @@ -3307,10 +3306,6 @@ function getParsedComment(text: string, parsingDetails?: ParsingDetails): string const currentReport = getReport(parsingDetails?.reportID); isGroupPolicyReport = isReportInGroupPolicy(currentReport); } - if (parsingDetails?.policyID) { - const policyType = getPolicy(parsingDetails?.policyID).type; - isGroupPolicyReport = isGroupPolicy(policyType); - } const parser = new ExpensiMark(); const textWithMention = text.replace(CONST.REGEX.SHORT_MENTION, (match) => { @@ -3380,7 +3375,14 @@ function buildOptimisticInviteReportAction(invitedUserDisplayName: string, invit }; } -function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number, createdOffset = 0, shouldEscapeText?: boolean, reportID?: string): OptimisticReportAction { +function buildOptimisticAddCommentReportAction( + text?: string, + file?: FileObject, + actorAccountID?: number, + createdOffset = 0, + shouldEscapeText?: boolean, + reportID?: string, +): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? '', {shouldEscapeText, reportID}); const isAttachmentOnly = file && !text; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index f02091cbfd6d..8ae0e2257705 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -1736,7 +1736,7 @@ function updateWorkspaceDescription(policyID: string, description: string, curre if (description === currentDescription) { return; } - const parsedDescription = ReportUtils.getParsedComment(description, {policyID}); + const parsedDescription = ReportUtils.getParsedComment(description); const optimisticData: OnyxUpdate[] = [ { diff --git a/src/pages/iou/HoldReasonPage.tsx b/src/pages/iou/HoldReasonPage.tsx index 93018fe34e5e..18b290a81ea4 100644 --- a/src/pages/iou/HoldReasonPage.tsx +++ b/src/pages/iou/HoldReasonPage.tsx @@ -49,7 +49,7 @@ function HoldReasonPage({route}: HoldReasonPageProps) { // We first check if the report is part of a policy - if not, then it's a personal request (1:1 request) // For personal requests, we need to allow both users to put the request on hold - const isWorkspaceRequest = ReportUtils.isGroupPolicy(report); + const isWorkspaceRequest = ReportUtils.isReportInGroupPolicy(report); const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); const navigateBack = () => { diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 6034b3b9fffc..5716812ced16 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -105,7 +105,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli */ const submit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.NEW_ROOM_FORM>) => { const participants = [session?.accountID ?? 0]; - const parsedDescription = ReportUtils.getParsedComment(values.reportDescription ?? '', {policyID}); + const parsedDescription = ReportUtils.getParsedComment(values.reportDescription ?? ''); const policyReport = ReportUtils.buildOptimisticChatReport( participants, values.roomName, @@ -183,7 +183,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli ErrorUtils.addErrorMessage(errors, 'roomName', ['common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); } - const descriptionLength = ReportUtils.getCommentLength(values.reportDescription, {policyID}); + const descriptionLength = ReportUtils.getCommentLength(values.reportDescription); if (descriptionLength > CONST.REPORT_DESCRIPTION.MAX_LENGTH) { ErrorUtils.addErrorMessage(errors, 'reportDescription', [ 'common.error.characterLimitExceedCounter', @@ -197,7 +197,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli return errors; }, - [reports, policyID], + [reports], ); const writeCapabilityOptions = useMemo( From 596c99758a1c5f3f87a1fdde701f53b67330e5d7 Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Tue, 30 Apr 2024 12:22:43 +0200 Subject: [PATCH 571/580] improve TransactionListItem layout on narrow screens --- src/components/Search.tsx | 14 +- .../SelectionList/TransactionListItem.tsx | 153 ++++++++---------- src/components/SelectionList/types.ts | 2 +- src/pages/Search/SearchTableHeader.tsx | 41 ----- src/types/onyx/SearchResults.ts | 8 +- 5 files changed, 78 insertions(+), 140 deletions(-) delete mode 100644 src/pages/Search/SearchTableHeader.tsx diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 4269cf812f36..8ddd79c81cc8 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -43,7 +43,7 @@ function Search({query}: SearchProps) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); const styles = useThemeStyles(); - const {isSmallScreenWidth} = useWindowDimensions(); + const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions(); // const [selectedCategories, setSelectedCategories] = useState<Record<string, boolean>>({}); useCustomBackHandler(); @@ -73,13 +73,15 @@ function Search({query}: SearchProps) { return <EmptySearchView />; } + const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; + const getListHeader = () => { - if (isSmallScreenWidth) { + if (displayNarrowVersion) { return; } // const showMerchantColumn = ReportUtils.shouldShowMerchantColumn(data); - const showMerchantColumn = isSmallScreenWidth && true; + const showMerchantColumn = displayNarrowVersion && true; return ( <View style={[styles.flex1, styles.flexRow, styles.justifyContentBetween, styles.pl3, styles.gap3]}> @@ -87,8 +89,8 @@ function Search({query}: SearchProps) { <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.date')}</Text> {showMerchantColumn && <Text style={[styles.searchInputStyle]}>{translate('common.merchant')}</Text>} <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.description')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.from')}</Text> - <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.to')}</Text> + <Text style={[styles.searchInputStyle, styles.flex2]}>{translate('common.from')}</Text> + <Text style={[styles.searchInputStyle, styles.flex2]}>{translate('common.to')}</Text> <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.category')}</Text> <Text style={[styles.searchInputStyle, styles.flex1]}>{translate('common.tag')}</Text> <Text style={[styles.searchInputStyle, styles.flex1, styles.textAlignRight]}>{translate('common.total')}</Text> @@ -118,7 +120,7 @@ function Search({query}: SearchProps) { onSelectRow={(item) => { openReport(item.transactionThreadReportID); }} - onSelectAll={!isSmallScreenWidth ? () => {} : undefined} + onSelectAll={!displayNarrowVersion ? () => {} : undefined} onCheckboxPress={() => {}} shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 938c53879de5..3ba7350739b1 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -16,9 +16,9 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {SearchTransactionType} from '@src/types/onyx/SearchResults'; +import type {SearchTransaction, SearchTransactionType} from '@src/types/onyx/SearchResults'; import BaseListItem from './BaseListItem'; -import type {ListItem, TransactionListItemProps} from './types'; +import type {TransactionListItemProps} from './types'; const getTypeIcon = (type: SearchTransactionType) => { switch (type) { @@ -33,7 +33,7 @@ const getTypeIcon = (type: SearchTransactionType) => { } }; -function TransactionListItem<TItem extends ListItem>({ +function TransactionListItem({ item, isFocused, showTooltip, @@ -46,15 +46,15 @@ function TransactionListItem<TItem extends ListItem>({ rightHandSideComponent, onFocus, shouldSyncFocus, -}: TransactionListItemProps<TItem>) { +}: TransactionListItemProps<SearchTransaction>) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); - const {isSmallScreenWidth} = useWindowDimensions(); + const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions(); const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; - const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; - const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; + // const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; + // const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; const typeIcon = getTypeIcon(item.type); @@ -66,6 +66,9 @@ function TransactionListItem<TItem extends ListItem>({ } }, [item, onCheckboxPress, onSelectRow]); + const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; + const userFontStyle = displayNarrowVersion ? styles.textMicro : undefined; + const rowButtonElement = ( <Button success @@ -89,7 +92,7 @@ function TransactionListItem<TItem extends ListItem>({ const categoryElement = ( <TextWithTooltip shouldShowTooltip={showTooltip} - text={item.category} + text={item.category ?? ''} style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} /> ); @@ -110,13 +113,49 @@ function TransactionListItem<TItem extends ListItem>({ /> ); - const displayNarrowVersion = isSmallScreenWidth; + const fromElement = ( + <View style={[styles.flexRow, styles.flex2, styles.gap3, styles.alignItemsCenter]}> + <Avatar + imageStyles={[styles.alignSelfCenter]} + size={CONST.AVATAR_SIZE.SMALL} + source={personalDetails[item.accountID]?.avatar} + name={personalDetails[item.accountID]?.displayName} + type={CONST.ICON_TYPE_WORKSPACE} + /> + <Text + numberOfLines={1} + style={[styles.flex1, styles.flexGrow1, styles.textStrong, userFontStyle]} + > + {personalDetails[item.accountID]?.displayName} + </Text> + </View> + ); + + const toElement = ( + <View style={[styles.flexRow, styles.gap3, styles.flex2, styles.alignItemsCenter]}> + <Avatar + imageStyles={[styles.alignSelfCenter]} + size={CONST.AVATAR_SIZE.SMALL} + source={personalDetails[item.managerID]?.avatar} + name={personalDetails[item.managerID]?.displayName} + type={CONST.ICON_TYPE_WORKSPACE} + /> + <Text + numberOfLines={1} + style={[styles.flex1, styles.flexGrow1, styles.textStrong, userFontStyle]} + > + {personalDetails[item.managerID]?.displayName} + </Text> + </View> + ); + + const listItemPressableStyle = [styles.selectionListPressableItemWrapper, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive]; if (displayNarrowVersion) { return ( <BaseListItem item={item} - pressableStyle={[[styles.selectionListPressableItemWrapper, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive]]} + pressableStyle={listItemPressableStyle} wrapperStyle={[styles.flexColumn, styles.flex1, styles.userSelectNone, styles.alignItemsStretch]} containerStyle={[styles.mb3]} isFocused={isFocused} @@ -134,48 +173,20 @@ function TransactionListItem<TItem extends ListItem>({ shouldSyncFocus={shouldSyncFocus} hoverStyle={item.isSelected && styles.activeComponentBG} > - {(hovered) => ( + {() => ( <> <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween, styles.mb2]}> <View style={[styles.flexRow, styles.flex1, styles.alignItemsCenter, styles.gap3]}> - <View style={[styles.flexRow, styles.flex1, styles.gap3, styles.alignItemsCenter, styles.justifyContentBetween]}> - <Avatar - imageStyles={[styles.alignSelfCenter]} - size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.accountID]?.avatar} - name={personalDetails[item.accountID]?.displayName} - type={CONST.ICON_TYPE_WORKSPACE} - /> - <Text - numberOfLines={1} - style={[styles.flex1, styles.flexGrow1, styles.textStrong, styles.textMicro]} - > - {personalDetails[item.accountID]?.displayName} - </Text> - </View> + {fromElement} <Icon src={Expensicons.ArrowRightLong} width={variables.iconSizeXXSmall} height={variables.iconSizeXXSmall} fill={theme.icon} /> - <View style={[styles.flexRow, styles.flex1, styles.gap3, styles.alignItemsCenter]}> - <Avatar - imageStyles={[styles.alignSelfCenter]} - size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.managerID]?.avatar} - name={personalDetails[item.managerID]?.displayName} - type={CONST.ICON_TYPE_WORKSPACE} - /> - <Text - numberOfLines={1} - style={[styles.flex1, styles.flexGrow1, styles.textStrong, styles.textMicro]} - > - {personalDetails[item.managerID]?.displayName} - </Text> - </View> + {toElement} </View> - <View style={styles.flexShrink0}>{rowButtonElement}</View> + {rowButtonElement} </View> <View style={[styles.flexRow, styles.justifyContentBetween]}> <View> @@ -202,7 +213,7 @@ function TransactionListItem<TItem extends ListItem>({ return ( <BaseListItem item={item} - pressableStyle={[[styles.selectionListPressableItemWrapper, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive]]} + pressableStyle={listItemPressableStyle} wrapperStyle={[styles.flexRow, styles.flex1, styles.justifyContentBetween, styles.userSelectNone, styles.alignItemsCenter]} containerStyle={[styles.mb3]} isFocused={isFocused} @@ -220,7 +231,7 @@ function TransactionListItem<TItem extends ListItem>({ shouldSyncFocus={shouldSyncFocus} hoverStyle={item.isSelected && styles.activeComponentBG} > - {(hovered) => ( + {() => ( <> {canSelectMultiple && ( <PressableWithFeedback @@ -244,61 +255,27 @@ function TransactionListItem<TItem extends ListItem>({ </PressableWithFeedback> )} <View style={[styles.flexRow, styles.flex1, styles.gap3]}> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>{dateElement}</View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>{descriptionElement}</View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <View style={[styles.flexRow, styles.gap3, styles.flex1, styles.alignItemsCenter]}> - <Avatar - imageStyles={[styles.alignSelfCenter]} - size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.accountID]?.avatar} - name={personalDetails[item.accountID]?.displayName} - type={CONST.ICON_TYPE_WORKSPACE} - /> - <Text - numberOfLines={1} - style={[styles.flex1, styles.flexGrow1, styles.textStrong]} - > - {personalDetails[item.accountID]?.displayName} - </Text> - </View> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> - <View style={[styles.flexRow, styles.gap3, styles.flex1, styles.alignItemsCenter]}> - <Avatar - imageStyles={[styles.alignSelfCenter]} - size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.managerID]?.avatar} - name={personalDetails[item.managerID]?.displayName} - type={CONST.ICON_TYPE_WORKSPACE} - /> - <Text - numberOfLines={1} - style={[styles.flex1, styles.flexGrow1, styles.textStrong]} - > - {personalDetails[item.managerID]?.displayName} - </Text> - </View> - </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>{categoryElement}</View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}>{dateElement}</View> + <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}>{descriptionElement}</View> + <View style={[styles.flex2, styles.justifyContentCenter, styles.alignItemsStretch]}>{toElement}</View> + <View style={[styles.flex2, styles.justifyContentCenter, styles.alignItemsStretch]}>{fromElement}</View> + <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}>{categoryElement}</View> + <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}> <TextWithTooltip shouldShowTooltip={showTooltip} - text={item.tag} + text={item.tag ?? ''} style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} /> </View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsEnd]}>{amountElement}</View> - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}> + <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsEnd]}>{amountElement}</View> + <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}> <Icon src={typeIcon} fill={theme.icon} /> </View> - - <View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch]}>{rowButtonElement}</View> + <View style={[styles.flex1]}>{rowButtonElement}</View> </View> - {!!item.rightElement && item.rightElement} </> )} </BaseListItem> diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index b72bba02ee19..562bf2388e38 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -7,10 +7,10 @@ import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import type IconAsset from '@src/types/utils/IconAsset'; -import type TransactionListItem from './TransactionListItem'; import type InviteMemberListItem from './InviteMemberListItem'; import type RadioListItem from './RadioListItem'; import type TableListItem from './TableListItem'; +import type TransactionListItem from './TransactionListItem'; import type UserListItem from './UserListItem'; type TRightHandSideComponent<TItem extends ListItem> = { diff --git a/src/pages/Search/SearchTableHeader.tsx b/src/pages/Search/SearchTableHeader.tsx deleted file mode 100644 index 77fd5300d6aa..000000000000 --- a/src/pages/Search/SearchTableHeader.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as ReportUtils from '@libs/ReportUtils'; -import type {Transaction} from '@src/types/onyx'; - -type SearchTableHeaderProps = { - data?: Transaction[]; - onSelectAll?: () => void; -}; - -function SearchTableHeader({data, onSelectAll}: SearchTableHeaderProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {isSmallScreenWidth} = useWindowDimensions(); - - // const showMerchantColumn = ReportUtils.shouldShowMerchantColumn(data); - const showMerchantColumn = isSmallScreenWidth && true; - - return ( - <View style={[styles.flexRow, styles.justifyContentBetween, styles.pl3, styles.pr9]}> - <Text style={styles.searchInputStyle}>{translate('common.receipt')}</Text> - <Text style={[styles.searchInputStyle]}>{translate('common.date')}</Text> - {showMerchantColumn && <Text style={[styles.searchInputStyle]}>{translate('common.merchant')}</Text>} - <Text style={[styles.searchInputStyle]}>{translate('common.description')}</Text> - <Text style={[styles.searchInputStyle, styles.textAlignCenter]}>{translate('common.from')}</Text> - <Text style={[styles.searchInputStyle, styles.textAlignCenter]}>{translate('common.to')}</Text> - <Text style={[styles.searchInputStyle, styles.textAlignCenter]}>{translate('common.category')}</Text> - <Text style={[styles.searchInputStyle, styles.textAlignCenter, styles.textAlignRight]}>{translate('common.total')}</Text> - <Text style={[styles.searchInputStyle, styles.textAlignCenter, styles.textAlignRight]}>{translate('common.type')}</Text> - <Text style={[styles.searchInputStyle, styles.textAlignCenter, styles.textAlignRight]}>{translate('common.action')}</Text> - </View> - ); -} - -SearchTableHeader.displayName = 'SearchTableHeader'; - -export default SearchTableHeader; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index f86f688f7819..3cd2bdd2610c 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -1,5 +1,5 @@ -import {ValueOf} from 'type-fest'; -import CONST from '@src/CONST'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; import type {Receipt} from './Transaction'; type SearchResultsInfo = { @@ -18,8 +18,8 @@ type SearchTransaction = { modifiedCreated?: string; modifiedMerchant?: string; description: string; - from: {displayName: string; avatarURL: string}; - to: {displayName: string; avatarURL: string}; + accountID: number; + managerID: string; amount: number; modifiedAmount?: number; category?: string; From aa625fae50d51b0a8b79bd32fe41f808a69e3f4e Mon Sep 17 00:00:00 2001 From: Mateusz Titz <titzmateusz@gmail.com> Date: Tue, 30 Apr 2024 14:38:16 +0200 Subject: [PATCH 572/580] add more small fixes to TransactionListItem --- src/components/SelectionList/TransactionListItem.tsx | 10 +++++----- src/types/onyx/SearchResults.ts | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 3ba7350739b1..5ea09d8ac24f 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -72,12 +72,12 @@ function TransactionListItem({ const rowButtonElement = ( <Button success + text="View" // Todo add translate onPress={() => { onSelectRow(item); }} small pressOnEnter - text="View" /> ); @@ -114,7 +114,7 @@ function TransactionListItem({ ); const fromElement = ( - <View style={[styles.flexRow, styles.flex2, styles.gap3, styles.alignItemsCenter]}> + <View style={[styles.flexRow, styles.flex1, styles.gap3, styles.alignItemsCenter]}> <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} @@ -132,7 +132,7 @@ function TransactionListItem({ ); const toElement = ( - <View style={[styles.flexRow, styles.gap3, styles.flex2, styles.alignItemsCenter]}> + <View style={[styles.flexRow, styles.flex1, styles.gap3, styles.alignItemsCenter]}> <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} @@ -176,7 +176,7 @@ function TransactionListItem({ {() => ( <> <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween, styles.mb2]}> - <View style={[styles.flexRow, styles.flex1, styles.alignItemsCenter, styles.gap3]}> + <View style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, styles.flex1]}> {fromElement} <Icon src={Expensicons.ArrowRightLong} @@ -257,8 +257,8 @@ function TransactionListItem({ <View style={[styles.flexRow, styles.flex1, styles.gap3]}> <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}>{dateElement}</View> <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}>{descriptionElement}</View> - <View style={[styles.flex2, styles.justifyContentCenter, styles.alignItemsStretch]}>{toElement}</View> <View style={[styles.flex2, styles.justifyContentCenter, styles.alignItemsStretch]}>{fromElement}</View> + <View style={[styles.flex2, styles.justifyContentCenter, styles.alignItemsStretch]}>{toElement}</View> <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}>{categoryElement}</View> <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}> <TextWithTooltip diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 3cd2bdd2610c..75f1738db86f 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -23,6 +23,7 @@ type SearchTransaction = { amount: number; modifiedAmount?: number; category?: string; + currency: string; tag?: string; type: SearchTransactionType; hasViolation: boolean; From 1c217d5fb0c0d83705a54cf84e46818e7dedaf73 Mon Sep 17 00:00:00 2001 From: Artem Makushov <waterim3009@gmail.com> Date: Tue, 30 Apr 2024 15:35:39 +0200 Subject: [PATCH 573/580] fix userListitem --- src/components/SelectionList/UserListItem.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/SelectionList/UserListItem.tsx b/src/components/SelectionList/UserListItem.tsx index 68349293e134..a517a9f1ca15 100644 --- a/src/components/SelectionList/UserListItem.tsx +++ b/src/components/SelectionList/UserListItem.tsx @@ -26,6 +26,7 @@ function UserListItem<TItem extends ListItem>({ onCheckboxPress, onDismissError, shouldPreventDefaultFocusOnSelectRow, + shouldPreventEnterKeySubmit, rightHandSideComponent, onFocus, shouldSyncFocus, @@ -58,6 +59,7 @@ function UserListItem<TItem extends ListItem>({ onSelectRow={onSelectRow} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} + shouldPreventEnterKeySubmit={shouldPreventEnterKeySubmit} rightHandSideComponent={rightHandSideComponent} errors={item.errors} pendingAction={item.pendingAction} From 799591172ad13f09b3e7e3bfbe7e12e7c66077e6 Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Tue, 30 Apr 2024 17:14:30 +0200 Subject: [PATCH 574/580] Add fixes to Search --- src/components/Search.tsx | 12 +++--- .../SelectionList/TransactionListItem.tsx | 38 +++++++++---------- src/components/SelectionList/types.ts | 38 ++++++++++++++----- src/languages/es.ts | 2 + src/libs/SearchUtils.ts | 7 +++- src/types/onyx/SearchResults.ts | 4 +- 6 files changed, 64 insertions(+), 37 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 8ddd79c81cc8..ef69d571ae48 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -32,7 +32,7 @@ function getCleanSearchResults(searchResults: unknown) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error // eslint-disable-next-line no-underscore-dangle,@typescript-eslint/no-unsafe-return - return searchResults.snapshot_?.data; + return searchResults?.data; } type SearchProps = { @@ -47,10 +47,8 @@ function Search({query}: SearchProps) { // const [selectedCategories, setSelectedCategories] = useState<Record<string, boolean>>({}); useCustomBackHandler(); - // Todo bring back hash when api is updated - // const hash = SearchUtils.getQueryHash(query); - // const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); - const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}`); + const hash = SearchUtils.getQueryHash(query); + const [searchResults, searchResultsMeta] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); useEffect(() => { if (isOffline) { @@ -62,6 +60,10 @@ function Search({query}: SearchProps) { const cleanResults = getCleanSearchResults(searchResults); + useEffect(() => { + SearchUtils.addPersonalDetailsFromSearch(cleanResults?.personalDetailsList ?? {}); + }, [cleanResults]); + const isLoading = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || cleanResults === undefined; const shouldShowEmptyState = !isLoading && isEmptyObject(cleanResults); diff --git a/src/components/SelectionList/TransactionListItem.tsx b/src/components/SelectionList/TransactionListItem.tsx index 5ea09d8ac24f..e9e5d01dc89d 100644 --- a/src/components/SelectionList/TransactionListItem.tsx +++ b/src/components/SelectionList/TransactionListItem.tsx @@ -16,11 +16,11 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {SearchTransaction, SearchTransactionType} from '@src/types/onyx/SearchResults'; +import type {SearchTransactionType} from '@src/types/onyx/SearchResults'; import BaseListItem from './BaseListItem'; -import type {TransactionListItemProps} from './types'; +import type {ListItem, TransactionListItemProps} from './types'; -const getTypeIcon = (type: SearchTransactionType) => { +const getTypeIcon = (type?: SearchTransactionType) => { switch (type) { case CONST.SEARCH_TRANSACTION_TYPE.CASH: return Expensicons.Cash; @@ -33,7 +33,7 @@ const getTypeIcon = (type: SearchTransactionType) => { } }; -function TransactionListItem({ +function TransactionListItem<TItem extends ListItem>({ item, isFocused, showTooltip, @@ -43,10 +43,9 @@ function TransactionListItem({ onCheckboxPress, onDismissError, shouldPreventDefaultFocusOnSelectRow, - rightHandSideComponent, onFocus, shouldSyncFocus, -}: TransactionListItemProps<SearchTransaction>) { +}: TransactionListItemProps<TItem>) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); @@ -56,7 +55,7 @@ function TransactionListItem({ // const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; // const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar; - const typeIcon = getTypeIcon(item.type); + const typeIcon = getTypeIcon(item?.type as SearchTransactionType); const handleCheckboxPress = useCallback(() => { if (onCheckboxPress) { @@ -69,6 +68,9 @@ function TransactionListItem({ const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; const userFontStyle = displayNarrowVersion ? styles.textMicro : undefined; + const accountDetails = item.accountID ? personalDetails[item.accountID] : null; + const managerDetails = item.managerID ? personalDetails[item.managerID] : null; + const rowButtonElement = ( <Button success @@ -84,7 +86,7 @@ function TransactionListItem({ const amountElement = ( <TextWithTooltip shouldShowTooltip={showTooltip} - text={`${CurrencyUtils.getLocalizedCurrencySymbol(item.currency)}${item.amount}`} + text={`${item.currency ? CurrencyUtils.getLocalizedCurrencySymbol(item?.currency) : ''}${item.amount}`} style={[styles.optionDisplayName, styles.textNewKansasNormal, styles.pre, styles.justifyContentCenter]} /> ); @@ -100,7 +102,7 @@ function TransactionListItem({ const descriptionElement = ( <TextWithTooltip shouldShowTooltip={showTooltip} - text={item.description} + text={item?.description ?? ''} style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} /> ); @@ -108,7 +110,7 @@ function TransactionListItem({ const dateElement = ( <TextWithTooltip shouldShowTooltip={showTooltip} - text={format(new Date(item.created), 'MMM dd')} + text={item?.created ? format(new Date(item.created), 'MMM dd') : ''} style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} /> ); @@ -118,15 +120,15 @@ function TransactionListItem({ <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.accountID]?.avatar} - name={personalDetails[item.accountID]?.displayName} + source={accountDetails?.avatar} + name={accountDetails?.displayName} type={CONST.ICON_TYPE_WORKSPACE} /> <Text numberOfLines={1} style={[styles.flex1, styles.flexGrow1, styles.textStrong, userFontStyle]} > - {personalDetails[item.accountID]?.displayName} + {accountDetails?.displayName} </Text> </View> ); @@ -136,15 +138,15 @@ function TransactionListItem({ <Avatar imageStyles={[styles.alignSelfCenter]} size={CONST.AVATAR_SIZE.SMALL} - source={personalDetails[item.managerID]?.avatar} - name={personalDetails[item.managerID]?.displayName} + source={managerDetails?.avatar} + name={managerDetails?.displayName} type={CONST.ICON_TYPE_WORKSPACE} /> <Text numberOfLines={1} style={[styles.flex1, styles.flexGrow1, styles.textStrong, userFontStyle]} > - {personalDetails[item.managerID]?.displayName} + {managerDetails?.displayName} </Text> </View> ); @@ -165,7 +167,6 @@ function TransactionListItem({ onSelectRow={() => {}} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} - rightHandSideComponent={rightHandSideComponent} errors={item.errors} pendingAction={item.pendingAction} keyForList={item.keyForList} @@ -223,7 +224,6 @@ function TransactionListItem({ onSelectRow={() => {}} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} - rightHandSideComponent={rightHandSideComponent} errors={item.errors} pendingAction={item.pendingAction} keyForList={item.keyForList} @@ -263,7 +263,7 @@ function TransactionListItem({ <View style={[styles.flex1, styles.justifyContentCenter, styles.alignItemsStretch]}> <TextWithTooltip shouldShowTooltip={showTooltip} - text={item.tag ?? ''} + text={item?.tag ?? ''} style={[styles.optionDisplayName, styles.textNormalThemeText, styles.pre, styles.justifyContentCenter]} /> </View> diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 562bf2388e38..d5f33e841dba 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -123,6 +123,24 @@ type ListItem = { badgeText?: string; brickRoadIndicator?: BrickRoad | '' | null; + + managerID?: number; + + amount?: number; + + currency?: string; + + tag?: string; + + description?: string; + + category?: string; + + created?: string; + + merchant?: string; + + type?: string; }; type ListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & { @@ -367,22 +385,22 @@ type ExtendedSectionListData<TItem extends ListItem, TSection extends SectionWit type SectionListDataType<TItem extends ListItem> = ExtendedSectionListData<TItem, SectionWithIndexOffset<TItem>>; export type { + BaseListItemProps, BaseSelectionListProps, + ButtonOrCheckBoxRoles, CommonListItemProps, - Section, - SectionWithIndexOffset, - BaseListItemProps, - UserListItemProps, - RadioListItemProps, - TableListItemProps, + FlattenedSectionsReturn, InviteMemberListItemProps, - TransactionListItemProps, + ItemLayout, ListItem, ListItemProps, - FlattenedSectionsReturn, - ItemLayout, - ButtonOrCheckBoxRoles, + RadioListItemProps, + Section, SectionListDataType, + SectionWithIndexOffset, SelectionListHandle, + TableListItemProps, + TransactionListItemProps, + UserListItemProps, ValidListItem, }; diff --git a/src/languages/es.ts b/src/languages/es.ts index 84c32800f4b8..a36759951f6b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -319,6 +319,8 @@ export default { subtitleText3: '.', }, businessName: 'Nombre de la empresa', + type: 'Tipo', + action: 'Acción', }, location: { useCurrent: 'Usar ubicación actual', diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index f524fd5b6cc1..f9ef272cce96 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,3 +1,4 @@ +import Onyx, {OnyxEntry} from 'react-native-onyx'; import TransactionListItem from '@components/SelectionList/TransactionListItem'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -33,4 +34,8 @@ function getQueryHash(query: string): number { return UserUtils.hashText(query, 2 ** 32); } -export {getQueryHash, getListItem, getSections}; +function addPersonalDetailsFromSearch(personalDetails: OnyxEntry<OnyxTypes.PersonalDetailsList>) { + Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails); +} + +export {getQueryHash, getListItem, getSections, addPersonalDetailsFromSearch}; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index 75f1738db86f..de159a433f37 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -19,7 +19,7 @@ type SearchTransaction = { modifiedMerchant?: string; description: string; accountID: number; - managerID: string; + managerID: number; amount: number; modifiedAmount?: number; category?: string; @@ -44,4 +44,4 @@ type SearchResults = { export default SearchResults; -export type {SearchTransaction, SearchTransactionType, SearchQuery}; +export type {SearchQuery, SearchTransaction, SearchTransactionType}; From 9c2f5565ac9d4eaf297dff0bdb6631fbae196068 Mon Sep 17 00:00:00 2001 From: neil-marcellini <neil@expensify.com> Date: Tue, 30 Apr 2024 08:22:04 -0700 Subject: [PATCH 575/580] Fix lint by removing type assertions --- src/components/ReportActionItem/MoneyRequestView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 9373df336e78..6fc23e427bc0 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -195,11 +195,11 @@ function MoneyRequestView({ const currency = policy ? policy.outputCurrency : PolicyUtils.getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD; - const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : distanceRates[rateID as string] ?? {}; + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(currency) : distanceRates[rateID] ?? {}; const {unit} = mileageRate; - const rate = (transaction?.comment?.customUnit?.defaultP2PRate as number) ?? mileageRate.rate; + const rate = transaction?.comment?.customUnit?.defaultP2PRate ?? mileageRate.rate; - const distance = DistanceRequestUtils.convertToDistanceInMeters((transaction?.comment?.customUnit?.quantity as number) ?? 0, unit); + const distance = DistanceRequestUtils.convertToDistanceInMeters(transaction?.comment?.customUnit?.quantity ?? 0, unit); const rateToDisplay = DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); let merchantTitle = isEmptyMerchant ? '' : transactionMerchant; From 241b6026255ab489c67c928309e00e8ca7708505 Mon Sep 17 00:00:00 2001 From: Wojciech Boman <wojciechboman@gmail.com> Date: Tue, 30 Apr 2024 17:27:56 +0200 Subject: [PATCH 576/580] Fix lint --- src/components/Search.tsx | 2 +- src/libs/SearchUtils.ts | 7 +------ src/libs/actions/Search.ts | 9 +++++++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/Search.tsx b/src/components/Search.tsx index ef69d571ae48..4ea2c6de0813 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -61,7 +61,7 @@ function Search({query}: SearchProps) { const cleanResults = getCleanSearchResults(searchResults); useEffect(() => { - SearchUtils.addPersonalDetailsFromSearch(cleanResults?.personalDetailsList ?? {}); + SearchActions.addPersonalDetailsFromSearch(cleanResults?.personalDetailsList ?? {}); }, [cleanResults]); const isLoading = (!isOffline && isLoadingOnyxValue(searchResultsMeta)) || cleanResults === undefined; diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index f9ef272cce96..97809ab31041 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -1,4 +1,3 @@ -import Onyx, {OnyxEntry} from 'react-native-onyx'; import TransactionListItem from '@components/SelectionList/TransactionListItem'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -34,8 +33,4 @@ function getQueryHash(query: string): number { return UserUtils.hashText(query, 2 ** 32); } -function addPersonalDetailsFromSearch(personalDetails: OnyxEntry<OnyxTypes.PersonalDetailsList>) { - Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails); -} - -export {getQueryHash, getListItem, getSections, addPersonalDetailsFromSearch}; +export {getListItem, getQueryHash, getSections}; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 03179fae93cc..2f5704a683fc 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1,13 +1,22 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import * as API from '@libs/API'; import {READ_COMMANDS} from '@libs/API/types'; import * as SearchUtils from '@libs/SearchUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetailsList} from '@src/types/onyx'; function search(query: string) { const hash = SearchUtils.getQueryHash(query); API.read(READ_COMMANDS.SEARCH, {query, hash}); } +function addPersonalDetailsFromSearch(personalDetails: OnyxEntry<PersonalDetailsList>) { + return Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, personalDetails); +} + export { // eslint-disable-next-line import/prefer-default-export + addPersonalDetailsFromSearch, search, }; From 2859ec2125612e48c858f64e4ad2f7e5c90a974f Mon Sep 17 00:00:00 2001 From: neil-marcellini <neil@expensify.com> Date: Tue, 30 Apr 2024 08:40:44 -0700 Subject: [PATCH 577/580] Update getDistance util and use it more --- src/components/MoneyRequestConfirmationList.tsx | 2 +- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/TransactionUtils.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index a4005d6c63f1..ec740a4e9843 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -267,7 +267,7 @@ function MoneyRequestConfirmationList({ const currency = (mileageRate as MileageRate)?.currency ?? policyCurrency; - const distance = transaction?.comment?.customUnit?.quantity ?? 0; + const distance = TransactionUtils.getDistance(transaction); const taxRates = policy?.taxRates ?? null; // A flag for showing the categories field diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 6fc23e427bc0..f71a220d0c99 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -199,7 +199,7 @@ function MoneyRequestView({ const {unit} = mileageRate; const rate = transaction?.comment?.customUnit?.defaultP2PRate ?? mileageRate.rate; - const distance = DistanceRequestUtils.convertToDistanceInMeters(transaction?.comment?.customUnit?.quantity ?? 0, unit); + const distance = DistanceRequestUtils.convertToDistanceInMeters(TransactionUtils.getDistance(transaction), unit); const rateToDisplay = DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); let merchantTitle = isEmptyMerchant ? '' : transactionMerchant; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 74bc15f32487..36cbb4f1ecc5 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -356,8 +356,8 @@ function getMerchant(transaction: OnyxEntry<Transaction>): string { return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? ''; } -function getDistance(transaction: Transaction): number { - return transaction?.routes?.route0?.distance ?? 0; +function getDistance(transaction: Transaction | null): number { + return transaction?.comment?.customUnit?.quantity ?? 0; } /** From 806a9f2818c8579e96950b9668eeccb3d71d1641 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Tue, 30 Apr 2024 16:11:15 +0000 Subject: [PATCH 578/580] Update version to 1.4.68-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2b913f96c62f..0c1a162ecd90 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046800 - versionName "1.4.68-0" + versionCode 1001046801 + versionName "1.4.68-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7117078a1756..a208fcd2f9bc 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.68.0</string> + <string>1.4.68.1</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 42d3db845e0b..063e36282fb0 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.68.0</string> + <string>1.4.68.1</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index ed783f6c5f7c..385be0d997ce 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.68</string> <key>CFBundleVersion</key> - <string>1.4.68.0</string> + <string>1.4.68.1</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index 955b87726868..f19bc3afc215 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.68-0", + "version": "1.4.68-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.68-0", + "version": "1.4.68-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 72990eebdecc..060633b570f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.68-0", + "version": "1.4.68-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 159cadb29014df4a926827678ee257ab6db3aa79 Mon Sep 17 00:00:00 2001 From: Francois Laithier <francois@expensify.com> Date: Tue, 30 Apr 2024 10:58:10 -0700 Subject: [PATCH 579/580] Revert "Merge pull request #40624 from bernhardoj/fix/39673-prevent-multiple-open-report" This reverts commit c3435523bc3f655268c3f9a348da84996e5d3a61, reversing changes made to c4d9d4eb847fe96bfade7f7cdb533d27bbc56d86. --- src/pages/home/ReportScreen.tsx | 9 ++++++++- src/pages/home/report/ReportActionsView.tsx | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index aa7c623ac2b4..d84d610a171a 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -426,12 +426,19 @@ function ReportScreen({ return; } + // It is possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that + // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. + // If it doesn't exist, then we fetch the report from the API. + if (report.reportID && report.reportID === reportIDFromRoute && !reportMetadata?.isLoadingInitialReportActions) { + return; + } + if (!shouldFetchReport(report)) { return; } fetchReport(); - }, [report, fetchReport, reportIDFromRoute]); + }, [report, reportMetadata?.isLoadingInitialReportActions, fetchReport, reportIDFromRoute]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 3552ac16b236..cb904327e625 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -237,6 +237,24 @@ function ReportActionsView({ const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; + useEffect(() => { + if (reportActionID) { + return; + } + + const interactionTask = InteractionManager.runAfterInteractions(() => { + openReportIfNecessary(); + }); + // eslint-disable-next-line @typescript-eslint/no-misused-promises + if (interactionTask) { + return () => { + interactionTask.cancel(); + }; + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { if (!reportActionID || indexOfLinkedAction > -1) { return; From c8b4071bb9522ed85bc6555bc78b04fc45460b25 Mon Sep 17 00:00:00 2001 From: OSBotify <infra+osbotify@expensify.com> Date: Tue, 30 Apr 2024 18:48:29 +0000 Subject: [PATCH 580/580] Update version to 1.4.68-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 0c1a162ecd90..3a5a98bc2bea 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001046801 - versionName "1.4.68-1" + versionCode 1001046802 + versionName "1.4.68-2" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index a208fcd2f9bc..4b1977091d84 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ </dict> </array> <key>CFBundleVersion</key> - <string>1.4.68.1</string> + <string>1.4.68.2</string> <key>FullStory</key> <dict> <key>OrgId</key> diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 063e36282fb0..f026c42d743c 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>1.4.68.1</string> + <string>1.4.68.2</string> </dict> </plist> diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 385be0d997ce..c2012182c714 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ <key>CFBundleShortVersionString</key> <string>1.4.68</string> <key>CFBundleVersion</key> - <string>1.4.68.1</string> + <string>1.4.68.2</string> <key>NSExtension</key> <dict> <key>NSExtensionPointIdentifier</key> diff --git a/package-lock.json b/package-lock.json index f19bc3afc215..07c49278659e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.68-1", + "version": "1.4.68-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.68-1", + "version": "1.4.68-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 060633b570f6..81aea83d66fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.68-1", + "version": "1.4.68-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",