diff --git a/android/app/build.gradle b/android/app/build.gradle
index 2e85857a297a..e7b4a60e824b 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001035301
- versionName "1.3.53-1"
+ versionCode 1001035407
+ versionName "1.3.54-7"
}
flavorDimensions "default"
@@ -136,7 +136,7 @@ android {
signingConfig signingConfigs.debug
}
release {
- signingConfig signingConfigs.debug
+ signingConfig signingConfigs.release
productFlavors.production.signingConfig signingConfigs.release
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 06b97c4c758d..3eb22374f955 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -78,7 +78,7 @@ platform :android do
upload_to_play_store(
package_name: "com.expensify.chat",
json_key: './android/app/android-fastlane-json-key.json',
- aab: './android/app/build/outputs/bundle/release/app-release.aab',
+ aab: './android/app/build/outputs/bundle/productionRelease/app-production-release.aab',
track: 'internal',
rollout: '1.0'
)
diff --git a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify.xcscheme b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify.xcscheme
index 05e8fefc6dab..23e118a2ba00 100644
--- a/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify.xcscheme
+++ b/ios/NewExpensify.xcodeproj/xcshareddata/xcschemes/New Expensify.xcscheme
@@ -15,7 +15,7 @@
@@ -31,7 +31,7 @@
@@ -49,7 +49,7 @@
@@ -89,7 +89,7 @@
@@ -106,7 +106,7 @@
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 4c769a9f1bbd..3eb0a1d4d184 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.3.53
+ 1.3.54
CFBundleSignature
????
CFBundleURLTypes
@@ -32,7 +32,7 @@
CFBundleVersion
- 1.3.53.1
+ 1.3.54.7
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index eb8e731a3b94..a0bd08f51a90 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.3.53
+ 1.3.54
CFBundleSignature
????
CFBundleVersion
- 1.3.53.1
+ 1.3.54.7
diff --git a/package-lock.json b/package-lock.json
index 4ee7e1baf1ff..193ea3669bb2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.53-1",
+ "version": "1.3.54-7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.53-1",
+ "version": "1.3.54-7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 00d8c2f027fe..64901e53204a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.53-1",
+ "version": "1.3.54-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.",
diff --git a/src/CONST.js b/src/CONST.js
index 4c19965837d9..a0c6cbf2bcf3 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -448,6 +448,7 @@ const CONST = {
TASKEDITED: 'TASKEDITED',
TASKCANCELLED: 'TASKCANCELLED',
IOU: 'IOU',
+ MODIFIEDEXPENSE: 'MODIFIEDEXPENSE',
REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED',
RENAMED: 'RENAMED',
CHRONOSOOOLIST: 'CHRONOSOOOLIST',
@@ -1257,8 +1258,10 @@ const CONST = {
},
EDIT_REQUEST_FIELD: {
AMOUNT: 'amount',
+ CURRENCY: 'currency',
DATE: 'date',
DESCRIPTION: 'description',
+ MERCHANT: 'merchant',
},
FOOTER: {
EXPENSE_MANAGEMENT_URL: `${USE_EXPENSIFY_URL}/expense-management`,
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index a99a09063561..64b3b960581f 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -220,6 +220,8 @@ export default {
NEW_TASK_FORM: 'newTaskForm',
EDIT_TASK_FORM: 'editTaskForm',
MONEY_REQUEST_DESCRIPTION_FORM: 'moneyRequestDescriptionForm',
+ MONEY_REQUEST_AMOUNT_FORM: 'moneyRequestAmountForm',
+ MONEY_REQUEST_CREATED_FORM: 'moneyRequestCreatedForm',
NEW_CONTACT_METHOD_FORM: 'newContactMethodForm',
PAYPAL_FORM: 'payPalForm',
SETTINGS_STATUS_SET_FORM: 'settingsStatusSetForm',
diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js
index 93e86e5171a2..b72d0fdea119 100644
--- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js
+++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.js
@@ -88,6 +88,7 @@ function BaseAnchorForCommentsOnly({onPressIn, onPressOut, href = '', rel = '',
event.preventDefault();
linkProps.onPress();
}}
+ suppressHighlighting
// Add testID so it gets selected as an anchor tag by SelectionScraper
testID="a"
// eslint-disable-next-line react/jsx-props-no-spreading
diff --git a/src/components/Banner.js b/src/components/Banner.js
index bf2df7e3b0fc..7ff1ab8210dc 100644
--- a/src/components/Banner.js
+++ b/src/components/Banner.js
@@ -88,6 +88,7 @@ function Banner(props) {
{props.text}
diff --git a/src/components/CurrencySymbolButton.js b/src/components/CurrencySymbolButton.js
index 0c19ce0f63b3..bd94e5334e9b 100644
--- a/src/components/CurrencySymbolButton.js
+++ b/src/components/CurrencySymbolButton.js
@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import Text from './Text';
import styles from '../styles/styles';
import Tooltip from './Tooltip';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import CONST from '../CONST';
+import useLocalize from '../hooks/useLocalize';
const propTypes = {
/** Currency symbol of selected currency */
@@ -14,18 +14,25 @@ const propTypes = {
/** Function to call when currency button is pressed */
onCurrencyButtonPress: PropTypes.func.isRequired,
- ...withLocalizePropTypes,
+ /** Flag to indicate if the button should be disabled */
+ disabled: PropTypes.bool,
};
-function CurrencySymbolButton(props) {
+const defaultProps = {
+ disabled: false,
+};
+
+function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol, disabled}) {
+ const {translate} = useLocalize();
return (
-
+
- {props.currencySymbol}
+ {currencySymbol}
);
@@ -33,5 +40,6 @@ function CurrencySymbolButton(props) {
CurrencySymbolButton.propTypes = propTypes;
CurrencySymbolButton.displayName = 'CurrencySymbolButton';
+CurrencySymbolButton.defaultProps = defaultProps;
-export default withLocalize(CurrencySymbolButton);
+export default CurrencySymbolButton;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
index f7086486637d..1cc0acf50926 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
@@ -70,6 +70,7 @@ function AnchorRenderer(props) {
diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js
index c62cb3c5281e..5dba1c9c1b20 100644
--- a/src/components/MagicCodeInput.js
+++ b/src/components/MagicCodeInput.js
@@ -114,21 +114,35 @@ function MagicCodeInput(props) {
},
}));
- const validateAndSubmit = () => {
- const numbers = decomposeString(props.value, props.maxLength);
+ /**
+ * Validate the entered code and submit
+ *
+ * @param {String} value
+ */
+ const validateAndSubmit = (value) => {
+ const numbers = decomposeString(value, props.maxLength);
if (!props.shouldSubmitOnComplete || _.filter(numbers, (n) => ValidationUtils.isNumeric(n)).length !== props.maxLength || props.network.isOffline) {
return;
}
// Blurs the input and removes focus from the last input and, if it should submit
// on complete, it will call the onFulfill callback.
blurMagicCodeInput();
- props.onFulfill(props.value);
+ props.onFulfill(value);
};
- useNetwork({onReconnect: validateAndSubmit});
+ useNetwork({onReconnect: () => validateAndSubmit(props.value)});
useEffect(() => {
- validateAndSubmit();
+ if (!props.hasError) {
+ return;
+ }
+
+ // Focus the last input if an error occurred to allow for corrections
+ inputRefs.current[props.maxLength - 1].focus();
+ }, [props.hasError, props.maxLength]);
+
+ useEffect(() => {
+ validateAndSubmit(props.value);
// We have not added:
// + the editIndex as the dependency because we don't want to run this logic after focusing on an input to edit it after the user has completed the code.
@@ -179,6 +193,11 @@ function MagicCodeInput(props) {
const finalInput = composeToString(numbers);
props.onChangeText(finalInput);
+
+ // If the same number is pressed, we cannot depend on props.value in useEffect for re-submitting
+ if (props.value === finalInput) {
+ validateAndSubmit(finalInput);
+ }
};
/**
diff --git a/src/components/MoneyRequestDetails.js b/src/components/MoneyRequestDetails.js
deleted file mode 100644
index a690c31c000c..000000000000
--- a/src/components/MoneyRequestDetails.js
+++ /dev/null
@@ -1,231 +0,0 @@
-import React from 'react';
-import {withOnyx} from 'react-native-onyx';
-import {View} from 'react-native';
-import PropTypes from 'prop-types';
-import lodashGet from 'lodash/get';
-import iouReportPropTypes from '../pages/iouReportPropTypes';
-import withLocalize, {withLocalizePropTypes} from './withLocalize';
-import * as ReportUtils from '../libs/ReportUtils';
-import * as Expensicons from './Icon/Expensicons';
-import Text from './Text';
-import participantPropTypes from './participantPropTypes';
-import Avatar from './Avatar';
-import styles from '../styles/styles';
-import themeColors from '../styles/themes/default';
-import CONST from '../CONST';
-import withWindowDimensions from './withWindowDimensions';
-import compose from '../libs/compose';
-import ROUTES from '../ROUTES';
-import Icon from './Icon';
-import SettlementButton from './SettlementButton';
-import * as Policy from '../libs/actions/Policy';
-import ONYXKEYS from '../ONYXKEYS';
-import * as IOU from '../libs/actions/IOU';
-import * as CurrencyUtils from '../libs/CurrencyUtils';
-import MenuItemWithTopDescription from './MenuItemWithTopDescription';
-import DateUtils from '../libs/DateUtils';
-import reportPropTypes from '../pages/reportPropTypes';
-import * as UserUtils from '../libs/UserUtils';
-import OfflineWithFeedback from './OfflineWithFeedback';
-
-const propTypes = {
- /** The report currently being looked at */
- report: iouReportPropTypes.isRequired,
-
- /** The expense report or iou report (only will have a value if this is a transaction thread) */
- parentReport: iouReportPropTypes,
-
- /** The policy object for the current route */
- policy: PropTypes.shape({
- /** The name of the policy */
- name: PropTypes.string,
-
- /** The URL for the policy avatar */
- avatar: PropTypes.string,
- }),
-
- /** The chat report this report is linked to */
- chatReport: reportPropTypes,
-
- /** Personal details so we can get the ones for the report participants */
- personalDetails: PropTypes.objectOf(participantPropTypes).isRequired,
-
- /** Whether we're viewing a report with a single transaction in it */
- isSingleTransactionView: PropTypes.bool,
-
- /** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user email */
- email: PropTypes.string,
- }),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- isSingleTransactionView: false,
- chatReport: {},
- session: {
- email: null,
- },
- parentReport: {},
- policy: null,
-};
-
-function MoneyRequestDetails(props) {
- // These are only used for the single transaction view and not for expense and iou reports
- const {amount: transactionAmount, currency: transactionCurrency, comment: transactionDescription} = ReportUtils.getMoneyRequestAction(props.parentReportAction);
- const formattedTransactionAmount = transactionAmount && transactionCurrency && CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency);
- const transactionDate = lodashGet(props.parentReportAction, ['created']);
- const formattedTransactionDate = DateUtils.getDateStringFromISOTimestamp(transactionDate);
-
- const reportTotal = ReportUtils.getMoneyRequestTotal(props.report);
- const formattedAmount = CurrencyUtils.convertToDisplayString(reportTotal, props.report.currency);
- const moneyRequestReport = props.isSingleTransactionView ? props.parentReport : props.report;
- const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
- const isExpenseReport = ReportUtils.isExpenseReport(moneyRequestReport);
- const payeeName = isExpenseReport ? ReportUtils.getPolicyName(moneyRequestReport) : ReportUtils.getDisplayNameForParticipant(moneyRequestReport.managerID);
- const payeeAvatar = isExpenseReport
- ? ReportUtils.getWorkspaceAvatar(moneyRequestReport)
- : UserUtils.getAvatar(lodashGet(props.personalDetails, [moneyRequestReport.managerID, 'avatar']), moneyRequestReport.managerID);
- const isPayer =
- Policy.isAdminOfFreePolicy([props.policy]) || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(props.session, 'accountID', null) === moneyRequestReport.managerID);
- const shouldShowSettlementButton =
- moneyRequestReport.reportID && !isSettled && !props.isSingleTransactionView && isPayer && !moneyRequestReport.isWaitingOnBankAccount && reportTotal !== 0;
- const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport);
- const shouldShowPaypal = Boolean(lodashGet(props.personalDetails, [moneyRequestReport.ownerAccountID, 'payPalMeAddress']));
- let description = `${props.translate('iou.amount')} • ${props.translate('iou.cash')}`;
- if (isSettled) {
- description += ` • ${props.translate('iou.settledExpensify')}`;
- } else if (props.report.isWaitingOnBankAccount) {
- description += ` • ${props.translate('iou.pending')}`;
- }
-
- const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(props.report);
- return (
-
-
-
- {props.translate('common.to')}
-
-
-
-
-
- {payeeName}
-
- {isExpenseReport && (
-
- {props.translate('workspace.common.workspace')}
-
- )}
-
-
-
- {!props.isSingleTransactionView && {formattedAmount}}
- {!props.isSingleTransactionView && isSettled && (
-
-
-
- )}
- {shouldShowSettlementButton && !props.isSmallScreenWidth && (
-
- IOU.payMoneyRequest(paymentType, props.chatReport, props.report)}
- enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW}
- addBankAccountRoute={bankAccountRoute}
- shouldShowPaymentOptions
- />
-
- )}
-
-
- {shouldShowSettlementButton && props.isSmallScreenWidth && (
- IOU.payMoneyRequest(paymentType, props.chatReport, props.report)}
- enablePaymentsRoute={ROUTES.BANK_ACCOUNT_NEW}
- addBankAccountRoute={bankAccountRoute}
- shouldShowPaymentOptions
- />
- )}
-
- {props.isSingleTransactionView && (
- <>
- Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))}
- />
- Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))}
- />
- Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
- />
- >
- )}
-
-
- );
-}
-
-MoneyRequestDetails.displayName = 'MoneyRequestDetails';
-MoneyRequestDetails.propTypes = propTypes;
-MoneyRequestDetails.defaultProps = defaultProps;
-
-export default compose(
- withWindowDimensions,
- withLocalize,
- withOnyx({
- chatReport: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- parentReport: {
- key: (props) => `${ONYXKEYS.COLLECTION.REPORT}${props.report.parentReportID}`,
- },
- }),
-)(MoneyRequestDetails);
diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index dc8916bdaecb..598fe8d096d9 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -5,7 +5,9 @@ import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import reportPropTypes from '../../pages/reportPropTypes';
import ONYXKEYS from '../../ONYXKEYS';
-import withWindowDimensions from '../withWindowDimensions';
+import ROUTES from '../../ROUTES';
+import * as Policy from '../../libs/actions/Policy';
+import Navigation from '../../libs/Navigation/Navigation';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '../withCurrentUserPersonalDetails';
import compose from '../../libs/compose';
import MenuItemWithTopDescription from '../MenuItemWithTopDescription';
@@ -20,6 +22,7 @@ import DateUtils from '../../libs/DateUtils';
import * as CurrencyUtils from '../../libs/CurrencyUtils';
import EmptyStateBackgroundImage from '../../../assets/images/empty-state_background-fade.png';
import useLocalize from '../../hooks/useLocalize';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
const propTypes = {
/** The report currently being looked at */
@@ -28,6 +31,21 @@ const propTypes = {
/** The expense report or iou report (only will have a value if this is a transaction thread) */
parentReport: iouReportPropTypes,
+ /** The policy object for the current route */
+ policy: PropTypes.shape({
+ /** The name of the policy */
+ name: PropTypes.string,
+
+ /** The URL for the policy avatar */
+ avatar: PropTypes.string,
+ }),
+
+ /** Session info for the currently logged in user. */
+ session: PropTypes.shape({
+ /** Currently logged in user email */
+ email: PropTypes.string,
+ }),
+
/** Whether we should display the horizontal rule below the component */
shouldShowHorizontalRule: PropTypes.bool.isRequired,
@@ -36,22 +54,38 @@ const propTypes = {
const defaultProps = {
parentReport: {},
+ policy: null,
+ session: {
+ email: null,
+ },
};
-function MoneyRequestView(props) {
- const parentReportAction = ReportActionsUtils.getParentReportAction(props.report);
+function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, policy, session}) {
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const {translate} = useLocalize();
+
+ const parentReportAction = ReportActionsUtils.getParentReportAction(report);
const {amount: transactionAmount, currency: transactionCurrency, comment: transactionDescription} = ReportUtils.getMoneyRequestAction(parentReportAction);
const formattedTransactionAmount = transactionAmount && transactionCurrency && CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency);
const transactionDate = lodashGet(parentReportAction, ['created']);
const formattedTransactionDate = DateUtils.getDateStringFromISOTimestamp(transactionDate);
- const moneyRequestReport = props.parentReport;
+ const moneyRequestReport = parentReport;
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
- const {translate} = useLocalize();
+ const isAdmin = Policy.isAdminOfFreePolicy([policy]) && ReportUtils.isExpenseReport(moneyRequestReport);
+ const isRequestor = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === parentReportAction.actorAccountID;
+ const canEdit = !isSettled && (isAdmin || isRequestor);
+
+ let description = `${translate('iou.amount')} • ${translate('iou.cash')}`;
+ if (isSettled) {
+ description += ` • ${translate('iou.settledExpensify')}`;
+ } else if (report.isWaitingOnBankAccount) {
+ description += ` • ${translate('iou.pending')}`;
+ }
return (
-
+
Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))}
+ disabled={isSettled || !canEdit}
+ shouldShowRightIcon={canEdit}
+ onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))}
/>
Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))}
+ disabled={isSettled || !canEdit}
+ shouldShowRightIcon={canEdit}
+ onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))}
/>
Navigation.navigate(ROUTES.getEditRequestRoute(props.report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
+ disabled={isSettled || !canEdit}
+ shouldShowRightIcon={canEdit}
+ onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
/>
- {props.shouldShowHorizontalRule && }
+ {shouldShowHorizontalRule && }
);
}
@@ -93,11 +126,16 @@ MoneyRequestView.defaultProps = defaultProps;
MoneyRequestView.displayName = 'MoneyRequestView';
export default compose(
- withWindowDimensions,
withCurrentUserPersonalDetails,
withOnyx({
parentReport: {
- key: (props) => `${ONYXKEYS.COLLECTION.REPORT}${props.report.parentReportID}`,
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`,
+ },
+ policy: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
},
}),
)(MoneyRequestView);
diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js
index d05ebd4bfd09..cd0e699da505 100644
--- a/src/components/ReportActionItem/TaskPreview.js
+++ b/src/components/ReportActionItem/TaskPreview.js
@@ -25,7 +25,7 @@ import personalDetailsPropType from '../../pages/personalDetailsPropType';
const propTypes = {
/** All personal details asssociated with user */
- personalDetailsList: personalDetailsPropType,
+ personalDetailsList: PropTypes.objectOf(personalDetailsPropType),
/** The ID of the associated taskReport */
taskReportID: PropTypes.string.isRequired,
diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.js
index fb559ba29999..e0e5d25a1643 100644
--- a/src/components/ReportWelcomeText.js
+++ b/src/components/ReportWelcomeText.js
@@ -87,6 +87,7 @@ function ReportWelcomeText(props) {
Navigation.navigate(ROUTES.getReportDetailsRoute(props.report.reportID))}
+ suppressHighlighting
>
{ReportUtils.getReportName(props.report)}
@@ -105,6 +106,7 @@ function ReportWelcomeText(props) {
Navigation.navigate(ROUTES.getProfileRoute(accountID))}
+ suppressHighlighting
>
{displayName}
diff --git a/src/components/TextInputWithCurrencySymbol.js b/src/components/TextInputWithCurrencySymbol.js
index ef3fc3a1464a..06f2c62fedd8 100644
--- a/src/components/TextInputWithCurrencySymbol.js
+++ b/src/components/TextInputWithCurrencySymbol.js
@@ -31,6 +31,9 @@ const propTypes = {
/** Function to call when selection in text input is changed */
onSelectionChange: PropTypes.func,
+
+ /** Flag to indicate if the button should be disabled */
+ disabled: PropTypes.bool,
};
const defaultProps = {
@@ -39,6 +42,7 @@ const defaultProps = {
onCurrencyButtonPress: () => {},
selection: undefined,
onSelectionChange: () => {},
+ disabled: false,
};
function TextInputWithCurrencySymbol(props) {
@@ -55,6 +59,7 @@ function TextInputWithCurrencySymbol(props) {
);
diff --git a/src/components/TextLink.js b/src/components/TextLink.js
index a1b1cb4d1e8a..966c49ddbffe 100644
--- a/src/components/TextLink.js
+++ b/src/components/TextLink.js
@@ -71,6 +71,7 @@ function TextLink(props) {
onMouseDown={props.onMouseDown}
onKeyDown={openLinkIfEnterKeyPressed}
ref={props.forwardedRef}
+ suppressHighlighting
// eslint-disable-next-line react/jsx-props-no-spreading
{...rest}
>
diff --git a/src/components/ThumbnailImage.js b/src/components/ThumbnailImage.js
index 47516164864f..d68d7530839b 100644
--- a/src/components/ThumbnailImage.js
+++ b/src/components/ThumbnailImage.js
@@ -1,10 +1,11 @@
import lodashClamp from 'lodash/clamp';
import React, {useCallback, useState} from 'react';
-import {View} from 'react-native';
+import {View, Dimensions} from 'react-native';
import PropTypes from 'prop-types';
import ImageWithSizeCalculation from './ImageWithSizeCalculation';
import styles from '../styles/styles';
import * as StyleUtils from '../styles/StyleUtils';
+import * as DeviceCapabilities from '../libs/DeviceCapabilities';
import useWindowDimensions from '../hooks/useWindowDimensions';
const propTypes = {
@@ -41,12 +42,17 @@ const defaultProps = {
*/
function calculateThumbnailImageSize(width, height, windowHeight) {
+ if (!width || !height) {
+ return {};
+ }
// Width of the thumbnail works better as a constant than it does
// a percentage of the screen width since it is relative to each screen
// Note: Clamp minimum width 40px to support touch device
let thumbnailScreenWidth = lodashClamp(width, 40, 250);
const imageHeight = height / (width / thumbnailScreenWidth);
- let thumbnailScreenHeight = lodashClamp(imageHeight, 40, windowHeight * 0.4);
+ // On mWeb, when soft keyboard opens, window height changes, making thumbnail height inconsistent. We use screen height instead.
+ const screenHeight = DeviceCapabilities.canUseTouchScreen() ? Dimensions.get('screen').height : windowHeight;
+ let thumbnailScreenHeight = lodashClamp(imageHeight, 40, screenHeight * 0.4);
const aspectRatio = height / width;
// If thumbnail height is greater than its width, then the image is portrait otherwise landscape.
diff --git a/src/languages/en.js b/src/languages/en.js
index 09651e6ec05e..229166a3f858 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -411,6 +411,7 @@ export default {
other: 'Unexpected error, please try again later',
genericCreateFailureMessage: 'Unexpected error requesting money, please try again later',
genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later',
+ genericEditFailureMessage: 'Unexpected error editing the money request, please try again later',
},
},
notificationPreferencesPage: {
diff --git a/src/languages/es.js b/src/languages/es.js
index fa920c84ce35..0e24a386cd14 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -410,6 +410,7 @@ export default {
other: 'Error inesperado, por favor inténtalo más tarde',
genericCreateFailureMessage: 'Error inesperado solicitando dinero, Por favor, inténtalo más tarde',
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',
},
},
notificationPreferencesPage: {
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index b574bfd3d00e..7ea2cacbdde8 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -385,6 +385,8 @@ function getLastMessageTextForReport(report) {
} else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) {
const iouReport = ReportUtils.getReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction));
lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(iouReport, lastReportAction);
+ } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) {
+ lastMessageTextFromReport = ReportUtils.getModifiedExpenseMessage(lastReportAction);
} else {
lastMessageTextFromReport = report ? report.lastMessageText || '' : '';
diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js
index 6d777533360d..161dc540fca3 100644
--- a/src/libs/ReportActionsUtils.js
+++ b/src/libs/ReportActionsUtils.js
@@ -93,6 +93,14 @@ function isReportPreviewAction(reportAction) {
return lodashGet(reportAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
}
+/**
+ * @param {Object} reportAction
+ * @returns {Boolean}
+ */
+function isModifiedExpenseAction(reportAction) {
+ return lodashGet(reportAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE;
+}
+
function isWhisperAction(action) {
return (action.whisperedToAccountIDs || []).length > 0;
}
@@ -600,6 +608,7 @@ export {
isSentMoneyReportAction,
isDeletedParentAction,
isReportPreviewAction,
+ isModifiedExpenseAction,
getIOUReportIDFromReportActionPreview,
isMessageDeleted,
isWhisperAction,
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index c9fe359a7e88..aa72fc88d8cc 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1,4 +1,5 @@
import _ from 'underscore';
+import {format} from 'date-fns';
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import lodashIntersection from 'lodash/intersection';
@@ -13,6 +14,7 @@ import ROUTES from '../ROUTES';
import * as NumberUtils from './NumberUtils';
import * as NumberFormatUtils from './NumberFormatUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
+import * as TransactionUtils from './TransactionUtils';
import Permissions from './Permissions';
import DateUtils from './DateUtils';
import linkingConfig from './Navigation/linkingConfig';
@@ -459,7 +461,7 @@ function isConciergeChatReport(report) {
function shouldDisableDetailPage(report) {
const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []);
- if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report)) {
+ if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report)) {
return false;
}
if (participantAccountIDs.length === 1) {
@@ -1304,6 +1306,89 @@ function getReportPreviewMessage(report, reportAction = {}) {
return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount});
}
+/**
+ * Get the report action message when expense has been modified.
+ *
+ * @param {Object} reportAction
+ * @returns {String}
+ */
+function getModifiedExpenseMessage(reportAction) {
+ const reportActionOriginalMessage = lodashGet(reportAction, 'originalMessage', {});
+ if (_.isEmpty(reportActionOriginalMessage)) {
+ return `changed the request`;
+ }
+
+ const hasModifiedAmount =
+ _.has(reportActionOriginalMessage, 'oldAmount') &&
+ _.has(reportActionOriginalMessage, 'oldCurrency') &&
+ _.has(reportActionOriginalMessage, 'amount') &&
+ _.has(reportActionOriginalMessage, 'currency');
+ if (hasModifiedAmount) {
+ const oldCurrency = reportActionOriginalMessage.oldCurrency;
+ const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.oldAmount, oldCurrency);
+
+ const currency = reportActionOriginalMessage.currency;
+ const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage.amount, currency);
+
+ return `changed the request to ${amount} (previously ${oldAmount})`;
+ }
+
+ const hasModifiedComment = _.has(reportActionOriginalMessage, 'oldComment') && _.has(reportActionOriginalMessage, 'newComment');
+ if (hasModifiedComment) {
+ return `changed the request description to "${reportActionOriginalMessage.newComment}" (previously "${reportActionOriginalMessage.oldComment}")`;
+ }
+
+ const hasModifiedMerchant = _.has(reportActionOriginalMessage, 'oldMerchant') && _.has(reportActionOriginalMessage, 'merchant');
+ if (hasModifiedMerchant) {
+ return `changed the request merchant to "${reportActionOriginalMessage.merchant}" (previously "${reportActionOriginalMessage.oldMerchant}")`;
+ }
+
+ const hasModifiedCreated = _.has(reportActionOriginalMessage, 'oldCreated') && _.has(reportActionOriginalMessage, 'created');
+ if (hasModifiedCreated) {
+ // Take only the YYYY-MM-DD value as the original date includes timestamp
+ let formattedOldCreated = new Date(reportActionOriginalMessage.oldCreated);
+ formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING);
+ return `changed the request date to ${reportActionOriginalMessage.created} (previously ${formattedOldCreated})`;
+ }
+}
+
+/**
+ * Given the updates user made to the request, compose the originalMessage
+ * object of the modified expense action.
+ *
+ * At the moment, we only allow changing one transaction field at a time.
+ *
+ * @param {Object} oldTransaction
+ * @param {Object} transactionChanges
+ * @param {Boolen} isFromExpenseReport
+ * @returns {Object}
+ */
+function getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport) {
+ const originalMessage = {};
+
+ // Remark: Comment field is the only one which has new/old prefixes for the keys (newComment/ oldComment),
+ // all others have old/- pattern such as oldCreated/created
+ if (_.has(transactionChanges, 'comment')) {
+ originalMessage.oldComment = TransactionUtils.getDescription(oldTransaction);
+ originalMessage.newComment = transactionChanges.comment;
+ }
+ if (_.has(transactionChanges, 'created')) {
+ originalMessage.oldCreated = TransactionUtils.getCreated(oldTransaction);
+ originalMessage.created = transactionChanges.created;
+ }
+
+ // The amount is always a combination of the currency and the number value so when one changes we need to store both
+ // to match how we handle the modified expense action in oldDot
+ if (_.has(transactionChanges, 'amount') || _.has(transactionChanges, 'currency')) {
+ originalMessage.oldAmount = TransactionUtils.getAmount(oldTransaction, isFromExpenseReport);
+ originalMessage.amount = lodashGet(transactionChanges, 'amount', originalMessage.oldAmount);
+ originalMessage.oldCurrency = TransactionUtils.getCurrency(oldTransaction);
+ originalMessage.currency = lodashGet(transactionChanges, 'currency', originalMessage.oldCurrency);
+ }
+
+ return originalMessage;
+}
+
/**
* Get the title for a report.
*
@@ -1451,7 +1536,7 @@ function getReport(reportID) {
function navigateToDetailsPage(report) {
const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []);
- if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report)) {
+ if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report)) {
Navigation.navigate(ROUTES.getReportDetailsRoute(report.reportID));
return;
}
@@ -1882,6 +1967,47 @@ function buildOptimisticReportPreview(chatReport, iouReport, comment = '') {
};
}
+/**
+ * Builds an optimistic modified expense action with a randomly generated reportActionID.
+ *
+ * @param {Object} transactionThread
+ * @param {Object} oldTransaction
+ * @param {Object} transactionChanges
+ * @param {Object} isFromExpenseReport
+ * @returns {Object}
+ */
+function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransaction, transactionChanges, isFromExpenseReport) {
+ const originalMessage = getModifiedExpenseOriginalMessage(oldTransaction, transactionChanges, isFromExpenseReport);
+ return {
+ actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE,
+ actorAccountID: currentUserAccountID,
+ automatic: false,
+ avatar: lodashGet(currentUserPersonalDetails, 'avatar', UserUtils.getDefaultAvatar(currentUserAccountID)),
+ created: DateUtils.getDBTime(),
+ isAttachment: false,
+ message: [
+ {
+ // Currently we are composing the message from the originalMessage and message is only used in OldDot and not in the App
+ text: 'You',
+ style: 'strong',
+ type: CONST.REPORT.MESSAGE.TYPE.TEXT,
+ },
+ ],
+ originalMessage,
+ person: [
+ {
+ style: 'strong',
+ text: lodashGet(currentUserPersonalDetails, 'displayName', currentUserAccountID),
+ type: 'TEXT',
+ },
+ ],
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ reportActionID: NumberUtils.rand64(),
+ reportID: transactionThread.reportID,
+ shouldShow: true,
+ };
+}
+
/**
* Updates a report preview action that exists for an IOU report.
*
@@ -2208,6 +2334,7 @@ function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parent
reportName: title,
description,
ownerAccountID,
+ participantAccountIDs: assigneeAccountID && assigneeAccountID !== ownerAccountID ? [assigneeAccountID] : [],
managerID: assigneeAccountID,
type: CONST.REPORT.TYPE.TASK,
parentReportID,
@@ -3088,6 +3215,7 @@ export {
buildOptimisticExpenseReport,
buildOptimisticIOUReportAction,
buildOptimisticReportPreview,
+ buildOptimisticModifiedExpenseReportAction,
updateReportPreview,
buildOptimisticTaskReportAction,
buildOptimisticAddCommentReportAction,
@@ -3140,6 +3268,7 @@ export {
getParentReport,
getTaskParentReportActionIDInAssigneeReport,
getReportPreviewMessage,
+ getModifiedExpenseMessage,
shouldHideComposer,
getOriginalReportID,
canAccessReport,
diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js
index f88f53467ae8..a05cd377514c 100644
--- a/src/libs/TransactionUtils.js
+++ b/src/libs/TransactionUtils.js
@@ -1,7 +1,24 @@
+import Onyx from 'react-native-onyx';
+import {format} from 'date-fns';
+import lodashGet from 'lodash/get';
+import _ from 'underscore';
import CONST from '../CONST';
+import ONYXKEYS from '../ONYXKEYS';
import DateUtils from './DateUtils';
import * as NumberUtils from './NumberUtils';
+let allTransactions = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.TRANSACTION,
+ waitForCollectionCallback: true,
+ callback: (val) => {
+ if (!val) {
+ return;
+ }
+ allTransactions = val;
+ },
+});
+
/**
* Optimistically generate a transaction.
*
@@ -41,6 +58,103 @@ function buildOptimisticTransaction(amount, currency, reportID, comment = '', so
};
}
-export default {
- buildOptimisticTransaction,
-};
+/**
+ * Given the edit made to the money request, return an updated transaction object.
+ *
+ * @param {Object} transaction
+ * @param {Object} transactionChanges
+ * @param {Object} isFromExpenseReport
+ * @returns {Object}
+ */
+function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport) {
+ // Only changing the first level fields so no need for deep clone now
+ const updatedTransaction = _.clone(transaction);
+
+ // The comment property does not have its modifiedComment counterpart
+ if (_.has(transactionChanges, 'comment')) {
+ updatedTransaction.comment = {
+ ...updatedTransaction.comment,
+ comment: transactionChanges.comment,
+ };
+ }
+ if (_.has(transactionChanges, 'created')) {
+ updatedTransaction.modifiedCreated = transactionChanges.created;
+ }
+ if (_.has(transactionChanges, 'amount')) {
+ updatedTransaction.modifiedAmount = isFromExpenseReport ? -transactionChanges.amount : transactionChanges.amount;
+ }
+ if (_.has(transactionChanges, 'currency')) {
+ updatedTransaction.modifiedCurrency = transactionChanges.currency;
+ }
+ updatedTransaction.pendingAction = CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE;
+
+ return updatedTransaction;
+}
+
+/**
+ * Retrieve the particular transaction object given its ID.
+ *
+ * @param {String} transactionID
+ * @returns {Object}
+ */
+function getTransaction(transactionID) {
+ return lodashGet(allTransactions, `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {});
+}
+
+/**
+ * Return the comment field (referred to as description in the App) from the transaction.
+ * The comment does not have its modifiedComment counterpart.
+ *
+ * @param {Object} transaction
+ * @returns {String}
+ */
+function getDescription(transaction) {
+ return lodashGet(transaction, 'comment.comment', '');
+}
+
+/**
+ * Return the amount field from the transaction, return the modifiedAmount if present.
+ *
+ * @param {Object} transaction
+ * @param {Boolean} isFromExpenseReport
+ * @returns {Number}
+ */
+function getAmount(transaction, isFromExpenseReport) {
+ // In case of expense reports, the amounts are stored using an opposite sign
+ const multiplier = isFromExpenseReport ? -1 : 1;
+ const amount = lodashGet(transaction, 'modifiedAmount', 0);
+ if (amount) {
+ return multiplier * amount;
+ }
+ return multiplier * lodashGet(transaction, 'amount', 0);
+}
+
+/**
+ * Return the currency field from the transaction, return the modifiedCurrency if present.
+ *
+ * @param {Object} transaction
+ * @returns {String}
+ */
+function getCurrency(transaction) {
+ const currency = lodashGet(transaction, 'modifiedCurrency', '');
+ if (currency) {
+ return currency;
+ }
+ return lodashGet(transaction, 'currency', '');
+}
+
+/**
+ * Return the created field from the transaction, return the modifiedCreated if present.
+ *
+ * @param {Object} transaction
+ * @returns {String}
+ */
+function getCreated(transaction) {
+ const created = lodashGet(transaction, 'modifiedCreated', '');
+ if (created) {
+ return format(new Date(created), CONST.DATE.FNS_FORMAT_STRING);
+ }
+ return format(new Date(lodashGet(transaction, 'created', '')), CONST.DATE.FNS_FORMAT_STRING);
+}
+
+export {buildOptimisticTransaction, getUpdatedTransaction, getTransaction, getDescription, getAmount, getCurrency, getCreated};
diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js
index fe2c908dfd0b..4556f08eebdc 100644
--- a/src/libs/actions/App.js
+++ b/src/libs/actions/App.js
@@ -53,13 +53,13 @@ function confirmReadyToOpenApp() {
/**
* @param {Array} policies
- * @return {Object} map of policy id to lastUpdated
+ * @return {Array} array of policy ids
*/
-function getNonOptimisticPolicyIDToLastModifiedMap(policies) {
+function getNonOptimisticPolicyIDs(policies) {
return _.chain(policies)
- .reject((policy) => lodashGet(policy, 'pendingAction', '') === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)
- .map((policy) => [policy.id, policy.lastModified || 0])
- .object()
+ .reject((policy) => lodashGet(policy, 'pendingAction', null) === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD)
+ .pluck('id')
+ .compact()
.value();
}
@@ -133,7 +133,7 @@ function getPolicyParamsForOpenOrReconnect() {
waitForCollectionCallback: true,
callback: (policies) => {
Onyx.disconnect(connectionID);
- resolve({policyIDToLastModified: JSON.stringify(getNonOptimisticPolicyIDToLastModifiedMap(policies))});
+ resolve({policyIDList: getNonOptimisticPolicyIDs(policies)});
},
});
});
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 9d0a6279ca18..b15a897bb4aa 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -16,7 +16,7 @@ import * as ReportActionsUtils from '../ReportActionsUtils';
import * as IOUUtils from '../IOUUtils';
import * as OptionsListUtils from '../OptionsListUtils';
import DateUtils from '../DateUtils';
-import TransactionUtils from '../TransactionUtils';
+import * as TransactionUtils from '../TransactionUtils';
import * as ErrorUtils from '../ErrorUtils';
import * as UserUtils from '../UserUtils';
import * as Report from './Report';
@@ -789,6 +789,90 @@ function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccou
Report.notifyNewAction(groupData.chatReportID, currentUserAccountID);
}
+/**
+ * @param {String} transactionID
+ * @param {Number} transactionThreadReportID
+ * @param {Object} transactionChanges
+ */
+function editMoneyRequest(transactionID, transactionThreadReportID, transactionChanges) {
+ // STEP 1: Get all collections we're updating
+ const transactionThread = allReports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`];
+ const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ const iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.parentReportID}`];
+ const isFromExpenseReport = ReportUtils.isExpenseReport(iouReport);
+
+ // STEP 2: Build new modified expense report action.
+ const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport);
+ const updatedTransaction = TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport);
+ // STEP 3: Compute the IOU total and update the report preview message so LHN amount owed is correct
+ // STEP 4: Compose the optimistic data
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread.reportID}`,
+ value: {
+ [updatedReportAction.reportActionID]: updatedReportAction,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: updatedTransaction,
+ },
+ ];
+
+ const successData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread.reportID}`,
+ value: {
+ [updatedReportAction.reportActionID]: {pendingAction: null},
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {pendingAction: null},
+ },
+ ];
+
+ const failureData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread.reportID}`,
+ value: {
+ [updatedReportAction.reportActionID]: updatedReportAction,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: transaction,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.report}`,
+ value: iouReport,
+ },
+ ];
+
+ // STEP 6: Call the API endpoint
+ API.write(
+ 'EditMoneyRequest',
+ {
+ transactionID,
+ reportActionID: updatedReportAction.reportActionID,
+
+ // Using the getter methods here to ensure we pass modified field if present
+ created: TransactionUtils.getCreated(updatedTransaction),
+ amount: TransactionUtils.getAmount(updatedTransaction, isFromExpenseReport),
+ currency: TransactionUtils.getCurrency(updatedTransaction),
+ comment: TransactionUtils.getDescription(updatedTransaction),
+ },
+ {optimisticData, successData, failureData},
+ );
+}
+
/**
* @param {String} transactionID
* @param {Object} reportAction - the money request reportAction we are deleting
@@ -1251,12 +1335,6 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho
iouReport.reportID,
true,
);
- const optimisticPersonalDetailsListAction = {
- accountID: Number(recipient.accountID),
- avatar: UserUtils.getDefaultAvatarURL(Number(recipient.accountID)),
- displayName: recipient.displayName || recipient.login,
- login: recipient.login,
- };
const optimisticReportPreviewAction = ReportUtils.updateReportPreview(iouReport, ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID));
@@ -1312,11 +1390,6 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho
key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD,
value: {[iouReport.policyID]: paymentMethodType},
},
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- value: optimisticPersonalDetailsListAction,
- },
];
const successData = [
@@ -1556,6 +1629,7 @@ function navigateToNextPage(iou, iouType, reportID, report) {
}
export {
+ editMoneyRequest,
deleteMoneyRequest,
splitBill,
splitBillAndOpenReport,
diff --git a/src/pages/EditRequestAmountPage.js b/src/pages/EditRequestAmountPage.js
new file mode 100644
index 000000000000..b04de9ebfd5a
--- /dev/null
+++ b/src/pages/EditRequestAmountPage.js
@@ -0,0 +1,70 @@
+import React, {useCallback, useRef} from 'react';
+import {InteractionManager} from 'react-native';
+import {useFocusEffect} from '@react-navigation/native';
+import PropTypes from 'prop-types';
+import ScreenWrapper from '../components/ScreenWrapper';
+import HeaderWithBackButton from '../components/HeaderWithBackButton';
+import Navigation from '../libs/Navigation/Navigation';
+import useLocalize from '../hooks/useLocalize';
+import MoneyRequestAmountForm from './iou/steps/MoneyRequestAmountForm';
+
+const propTypes = {
+ /** Transaction default amount value */
+ defaultAmount: PropTypes.number.isRequired,
+
+ /** Transaction default currency value */
+ defaultCurrency: PropTypes.string.isRequired,
+
+ /** Callback to fire when the Save button is pressed */
+ onSubmit: PropTypes.func.isRequired,
+};
+
+function EditRequestAmountPage({defaultAmount, defaultCurrency, onSubmit}) {
+ const {translate} = useLocalize();
+ const textInput = useRef(null);
+
+ const focusTextInput = () => {
+ // Component may not be initialized due to navigation transitions
+ // Wait until interactions are complete before trying to focus
+ InteractionManager.runAfterInteractions(() => {
+ // Focus text input
+ if (!textInput.current) {
+ return;
+ }
+
+ textInput.current.focus();
+ });
+ };
+
+ useFocusEffect(
+ useCallback(() => {
+ focusTextInput();
+ }, []),
+ );
+
+ return (
+
+
+ (textInput.current = e)}
+ onCurrencyButtonPress={() => null}
+ onSubmitButtonPress={onSubmit}
+ />
+
+ );
+}
+
+EditRequestAmountPage.propTypes = propTypes;
+EditRequestAmountPage.displayName = 'EditRequestAmountPage';
+
+export default EditRequestAmountPage;
diff --git a/src/pages/EditRequestCreatedPage.js b/src/pages/EditRequestCreatedPage.js
new file mode 100644
index 000000000000..9d367d3008d7
--- /dev/null
+++ b/src/pages/EditRequestCreatedPage.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ScreenWrapper from '../components/ScreenWrapper';
+import HeaderWithBackButton from '../components/HeaderWithBackButton';
+import Form from '../components/Form';
+import ONYXKEYS from '../ONYXKEYS';
+import styles from '../styles/styles';
+import Navigation from '../libs/Navigation/Navigation';
+import useLocalize from '../hooks/useLocalize';
+import NewDatePicker from '../components/NewDatePicker';
+
+const propTypes = {
+ /** Transaction defailt created value */
+ defaultCreated: PropTypes.string.isRequired,
+
+ /** Callback to fire when the Save button is pressed */
+ onSubmit: PropTypes.func.isRequired,
+};
+
+function EditRequestCreatedPage({defaultCreated, onSubmit}) {
+ const {translate} = useLocalize();
+
+ return (
+
+
+
+
+ );
+}
+
+EditRequestCreatedPage.propTypes = propTypes;
+EditRequestCreatedPage.displayName = 'EditRequestCreatedPage';
+
+export default EditRequestCreatedPage;
diff --git a/src/pages/EditRequestDescriptionPage.js b/src/pages/EditRequestDescriptionPage.js
index 34f88f29dc28..eb909e8cc9b4 100644
--- a/src/pages/EditRequestDescriptionPage.js
+++ b/src/pages/EditRequestDescriptionPage.js
@@ -2,7 +2,6 @@ import React, {useRef} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import TextInput from '../components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import ScreenWrapper from '../components/ScreenWrapper';
import HeaderWithBackButton from '../components/HeaderWithBackButton';
import Form from '../components/Form';
@@ -10,18 +9,18 @@ import ONYXKEYS from '../ONYXKEYS';
import styles from '../styles/styles';
import Navigation from '../libs/Navigation/Navigation';
import CONST from '../CONST';
+import useLocalize from '../hooks/useLocalize';
const propTypes = {
- ...withLocalizePropTypes,
-
- /** Transaction description default value */
+ /** Transaction default description value */
defaultDescription: PropTypes.string.isRequired,
/** Callback to fire when the Save button is pressed */
onSubmit: PropTypes.func.isRequired,
};
-function EditRequestDescriptionPage(props) {
+function EditRequestDescriptionPage({defaultDescription, onSubmit}) {
+ const {translate} = useLocalize();
const descriptionInputRef = useRef(null);
return (
descriptionInputRef.current && descriptionInputRef.current.focus()}
>
Navigation.goBack()}
/>