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 (