diff --git a/android/app/build.gradle b/android/app/build.gradle
index db251e681e64..e20eb75ad461 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 1001038107
- versionName "1.3.81-7"
+ versionCode 1001038109
+ versionName "1.3.81-9"
}
flavorDimensions "default"
diff --git a/docs/assets/images/ExpensifyHelp_ExpenseRules_01.png b/docs/assets/images/ExpensifyHelp_ExpenseRules_01.png
new file mode 100644
index 000000000000..7a6c3c1b3a13
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_ExpenseRules_01.png differ
diff --git a/docs/assets/images/ExpensifyHelp_ExpenseRules_02.png b/docs/assets/images/ExpensifyHelp_ExpenseRules_02.png
new file mode 100644
index 000000000000..28c6a7689b77
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_ExpenseRules_02.png differ
diff --git a/docs/assets/images/ExpensifyHelp_ExpenseRules_03.png b/docs/assets/images/ExpensifyHelp_ExpenseRules_03.png
new file mode 100644
index 000000000000..90c9855c0c49
Binary files /dev/null and b/docs/assets/images/ExpensifyHelp_ExpenseRules_03.png differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index e797b5055dee..f7caac8d11a2 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.81.7
+ 1.3.81.9
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index dce89fb2b19a..e83c77350a70 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.81.7
+ 1.3.81.9
diff --git a/package-lock.json b/package-lock.json
index f92b6853a718..0530a08841ea 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.81-7",
+ "version": "1.3.81-9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.81-7",
+ "version": "1.3.81-9",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 77a0bb1e1664..1c2fd1eb9328 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.81-7",
+ "version": "1.3.81-9",
"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/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 0f62bac198f8..1b471f257965 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -195,6 +195,7 @@ function MoneyRequestConfirmationList(props) {
const isTypeRequest = props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST;
const isSplitBill = props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT;
+ const isTypeSend = props.iouType === CONST.IOU.MONEY_REQUEST_TYPE.SEND;
const {unit, rate, currency} = props.mileageRate;
const distance = lodashGet(transaction, 'routes.route0.distance', 0);
@@ -206,7 +207,13 @@ function MoneyRequestConfirmationList(props) {
// A flag and a toggler for showing the rest of the form fields
const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false);
- const shouldShowAllFields = props.isDistanceRequest || shouldExpandFields || props.isEditingSplitBill || !props.shouldShowSmartScanFields;
+
+ // Do not hide fields in case of send money request
+ const shouldShowAllFields = props.isDistanceRequest || shouldExpandFields || !props.shouldShowSmartScanFields || isTypeSend || props.isEditingSplitBill;
+
+ // In Send Money flow, we don't allow the Merchant or Date to be edited.
+ const shouldShowDate = shouldShowAllFields && !isTypeSend;
+ const shouldShowMerchant = shouldShowAllFields && !isTypeSend;
// Fetches the first tag list of the policy
const policyTag = PolicyUtils.getTag(props.policyTags);
@@ -472,6 +479,7 @@ function MoneyRequestConfirmationList(props) {
const button = shouldShowSettlementButton ? (
- {props.shouldShowSmartScanFields && (
+ {shouldShowDate && (
)}
- {props.shouldShowSmartScanFields && (
+ {shouldShowMerchant && (
selectPaymentType(event, iouPaymentType, triggerKYCFlow)}
+ pressOnEnter={pressOnEnter}
options={paymentButtonOptions}
style={style}
buttonSize={buttonSize}
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index c0b8b5620bfa..84381b49aea2 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -2121,6 +2121,7 @@ function buildOptimisticIOUReport(payeeAccountID, payerAccountID, total, chatRep
reportID: generateReportID(),
state: CONST.REPORT.STATE.SUBMITTED,
stateNum: isSendingMoney ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.PROCESSING,
+ statusNum: isSendingMoney ? CONST.REPORT.STATUS.REIMBURSED : CONST.REPORT.STATE_NUM.PROCESSING,
total,
// We don't translate reportName because the server response is always in English
diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js
index 927e9431816e..de0e0a16c214 100644
--- a/src/pages/iou/steps/MoneyRequestConfirmPage.js
+++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js
@@ -304,6 +304,10 @@ function MoneyRequestConfirmPage(props) {
return props.translate('iou.split');
}
+ if (iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SEND) {
+ return props.translate('common.send');
+ }
+
return props.translate('tabSelector.manual');
};
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
index bd3ea8a50402..25e41ba78556 100644
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
@@ -50,6 +50,7 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
const iouType = useRef(lodashGet(route, 'params.iouType', ''));
const reportID = useRef(lodashGet(route, 'params.reportID', ''));
const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType.current, selectedTab);
+ const isSendRequest = iouType.current === CONST.IOU.MONEY_REQUEST_TYPE.SEND;
const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab);
const isSplitRequest = iou.id === CONST.IOU.MONEY_REQUEST_TYPE.SPLIT;
const [headerTitle, setHeaderTitle] = useState();
@@ -60,8 +61,13 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
return;
}
+ if (isSendRequest) {
+ setHeaderTitle(translate('common.send'));
+ return;
+ }
+
setHeaderTitle(_.isEmpty(iou.participants) ? translate('tabSelector.manual') : translate('iou.split'));
- }, [iou.participants, isDistanceRequest, translate]);
+ }, [iou.participants, isDistanceRequest, isSendRequest, translate]);
const navigateToConfirmationStep = (moneyRequestType) => {
IOU.setMoneyRequestId(moneyRequestType);
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index bedca1a10c35..547d2b7c363a 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -240,7 +240,7 @@ function MoneyRequestParticipantsSelector({
// 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 shouldShowConfirmButton = !(participants.length > 1 && hasPolicyExpenseChatParticipant);
- const isAllowedToSplit = !isDistanceRequest;
+ const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.MONEY_REQUEST_TYPE.SEND;
return (
0 ? safeAreaPaddingBottomStyle : {}]}>
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index e7bbd9657e8b..4fa2494d5fdf 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -67,7 +67,7 @@ function WorkspaceInvitePage(props) {
const [searchTerm, setSearchTerm] = useState('');
const [selectedOptions, setSelectedOptions] = useState([]);
const [personalDetails, setPersonalDetails] = useState([]);
- const [userToInvite, setUserToInvite] = useState(null);
+ const [usersToInvite, setUsersToInvite] = useState([]);
const openWorkspaceInvitePage = () => {
const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails);
Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs));
@@ -84,19 +84,52 @@ function WorkspaceInvitePage(props) {
const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]);
useEffect(() => {
- const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers);
-
- // Update selectedOptions with the latest personalDetails and policyMembers information
- const detailsMap = {};
- _.forEach(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail)));
- const newSelectedOptions = [];
- _.forEach(selectedOptions, (option) => {
- newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option);
+ const emails = _.compact(
+ searchTerm
+ .trim()
+ .replace(/\s*,\s*/g, ',')
+ .split(','),
+ );
+
+ const newUsersToInviteDict = {};
+ const newPersonalDetailsDict = {};
+ const newSelectedOptionsDict = {};
+
+ _.each(emails, (email) => {
+ const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, email, excludedUsers);
+
+ // Update selectedOptions with the latest personalDetails and policyMembers information
+ const detailsMap = {};
+ _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail)));
+
+ const newSelectedOptions = [];
+ _.each(selectedOptions, (option) => {
+ newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option);
+ });
+
+ const userToInvite = inviteOptions.userToInvite;
+
+ // Only add the user to the invites list if it is valid
+ if (userToInvite) {
+ newUsersToInviteDict[userToInvite.accountID] = userToInvite;
+ }
+
+ // Add all personal details to the new dict
+ _.each(inviteOptions.personalDetails, (details) => {
+ newPersonalDetailsDict[details.accountID] = details;
+ });
+
+ // Add all selected options to the new dict
+ _.each(newSelectedOptions, (option) => {
+ newSelectedOptionsDict[option.accountID] = option;
+ });
});
- setUserToInvite(inviteOptions.userToInvite);
- setPersonalDetails(inviteOptions.personalDetails);
- setSelectedOptions(newSelectedOptions);
+ // Strip out dictionary keys and update arrays
+ setUsersToInvite(_.values(newUsersToInviteDict));
+ setPersonalDetails(_.values(newPersonalDetailsDict));
+ setSelectedOptions(_.values(newSelectedOptionsDict));
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change
}, [props.personalDetails, props.policyMembers, props.betas, searchTerm, excludedUsers]);
@@ -116,7 +149,6 @@ function WorkspaceInvitePage(props) {
const selectedLogins = _.map(selectedOptions, ({login}) => login);
const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login));
const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList);
- const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login);
sections.push({
title: translate('common.contacts'),
@@ -126,14 +158,18 @@ function WorkspaceInvitePage(props) {
});
indexOffset += personalDetailsFormatted.length;
- if (hasUnselectedUserToInvite) {
- sections.push({
- title: undefined,
- data: [OptionsListUtils.formatMemberForList(userToInvite)],
- shouldShow: true,
- indexOffset,
- });
- }
+ _.each(usersToInvite, (userToInvite) => {
+ const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login);
+
+ if (hasUnselectedUserToInvite) {
+ sections.push({
+ title: undefined,
+ data: [OptionsListUtils.formatMemberForList(userToInvite)],
+ shouldShow: true,
+ indexOffset: indexOffset++,
+ });
+ }
+ });
return sections;
};
@@ -188,14 +224,14 @@ function WorkspaceInvitePage(props) {
const headerMessage = useMemo(() => {
const searchValue = searchTerm.trim().toLowerCase();
- if (!userToInvite && CONST.EXPENSIFY_EMAILS.includes(searchValue)) {
+ if (usersToInvite.length === 0 && CONST.EXPENSIFY_EMAILS.includes(searchValue)) {
return translate('messages.errorMessageInvalidEmail');
}
- if (!userToInvite && excludedUsers.includes(searchValue)) {
+ if (usersToInvite.length === 0 && excludedUsers.includes(searchValue)) {
return translate('messages.userIsAlreadyMemberOfWorkspace', {login: searchValue, workspace: policyName});
}
- return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, Boolean(userToInvite), searchValue);
- }, [excludedUsers, translate, searchTerm, policyName, userToInvite, personalDetails]);
+ return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue);
+ }, [excludedUsers, translate, searchTerm, policyName, usersToInvite, personalDetails]);
return (