Skip to content

Commit

Permalink
Merge pull request #28976 from Expensify/marco-approveButtonCollectPo…
Browse files Browse the repository at this point in the history
…licies

Add Approve button for collect policies in the Settlement button
  • Loading branch information
marcochavezf authored Dec 12, 2023
2 parents 4029eed + 1128243 commit f454dd1
Show file tree
Hide file tree
Showing 16 changed files with 181 additions and 53 deletions.
8 changes: 8 additions & 0 deletions assets/images/thumbs-up.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,9 @@ const CONST = {
OWNER_ACCOUNT_ID_FAKE: 0,
DEFAULT_REPORT_NAME: 'Chat Report',
},
NEXT_STEP: {
FINISHED: 'Finished!',
},
COMPOSER: {
MAX_LINES: 16,
MAX_LINES_SMALL_SCREEN: 6,
Expand Down Expand Up @@ -1167,6 +1170,7 @@ const CONST = {
DECLINE: 'decline',
CANCEL: 'cancel',
DELETE: 'delete',
APPROVE: 'approve',
},
AMOUNT_MAX_LENGTH: 10,
RECEIPT_STATE: {
Expand Down
2 changes: 1 addition & 1 deletion src/components/ButtonWithDropdownMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ function ButtonWithDropdownMenu(props) {
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState(null);
const {windowWidth, windowHeight} = useWindowDimensions();
const caretButton = useRef(null);
const selectedItem = props.options[selectedItemIndex];
const selectedItem = props.options[selectedItemIndex] || _.first(props.options);
const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(props.buttonSize);
const isButtonSizeLarge = props.buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE;

Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import Youtube from '@assets/images/social-youtube.svg';
import Sync from '@assets/images/sync.svg';
import Task from '@assets/images/task.svg';
import ThreeDots from '@assets/images/three-dots.svg';
import ThumbsUp from '@assets/images/thumbs-up.svg';
import Transfer from '@assets/images/transfer.svg';
import Trashcan from '@assets/images/trashcan.svg';
import Unlock from '@assets/images/unlock.svg';
Expand Down Expand Up @@ -242,6 +243,7 @@ export {
Shield,
Sync,
Task,
ThumbsUp,
ThreeDots,
Transfer,
Trashcan,
Expand Down
39 changes: 13 additions & 26 deletions src/components/MoneyReportHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,19 +75,24 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
const policyType = lodashGet(policy, 'type');
const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN;
const isGroupPolicy = _.contains([CONST.POLICY.TYPE.CORPORATE, CONST.POLICY.TYPE.TEAM], policyType);
const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID;
const isPayer = policyType === CONST.POLICY.TYPE.CORPORATE ? isPolicyAdmin && isApproved : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const isPayer = isGroupPolicy
? // In a group policy, the admin approver can pay the report directly by skipping the approval step
isPolicyAdmin && (isApproved || isManager)
: isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport);
const shouldShowSettlementButton = useMemo(
const shouldShowPayButton = useMemo(
() => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport),
[isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport],
);
const shouldShowApproveButton = useMemo(() => {
if (policyType !== CONST.POLICY.TYPE.CORPORATE) {
if (!isGroupPolicy) {
return false;
}
return isManager && !isDraft && !isApproved && !isSettled;
}, [policyType, isManager, isDraft, isApproved, isSettled]);
}, [isGroupPolicy, isManager, isDraft, isApproved, isSettled]);
const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton;
const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0;
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
const shouldShowNextSteps = isFromPaidPolicy && nextStep && !_.isEmpty(nextStep.message);
Expand Down Expand Up @@ -120,22 +125,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
style={[styles.pv2]}
formattedAmount={formattedAmount}
/>
</View>
)}
{shouldShowApproveButton && !isSmallScreenWidth && (
<View style={styles.pv2}>
<Button
success
medium
text={translate('iou.approve')}
style={[styles.mnw120, styles.pv2, styles.pr0]}
onPress={() => IOU.approveMoneyRequest(moneyRequestReport)}
/>
</View>
)}
{shouldShowSubmitButton && !isSmallScreenWidth && (
<View style={styles.pv2}>
<Button
Expand All @@ -159,21 +155,12 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
formattedAmount={formattedAmount}
/>
</View>
)}
{shouldShowApproveButton && isSmallScreenWidth && (
<View style={[styles.ph5, styles.pb2]}>
<Button
success
medium
text={translate('iou.approve')}
style={[styles.w100, styles.pr0]}
onPress={() => IOU.approveMoneyRequest(moneyRequestReport)}
/>
</View>
)}
{shouldShowSubmitButton && isSmallScreenWidth && (
<View style={[styles.ph5, styles.pb2]}>
<Button
Expand Down
3 changes: 2 additions & 1 deletion src/components/MoneyReportHeaderStatusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useLocalize from '@hooks/useLocalize';
import * as NextStepUtils from '@libs/NextStepUtils';
import nextStepPropTypes from '@pages/nextStepPropTypes';
import useThemeStyles from '@styles/useThemeStyles';
import CONST from '@src/CONST';
import RenderHTML from './RenderHTML';

const propTypes = {
Expand All @@ -27,7 +28,7 @@ function MoneyReportHeaderStatusBar({nextStep}) {
return (
<View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, styles.overflowHidden, styles.w100]}>
<View style={styles.moneyRequestHeaderStatusBarBadge}>
<Text style={[styles.textLabel, styles.textMicroBold]}>{translate('iou.nextSteps')}</Text>
<Text style={[styles.textLabel, styles.textMicroBold]}>{translate(nextStep.title === CONST.NEXT_STEP.FINISHED ? 'iou.finished' : 'iou.nextSteps')}</Text>
</View>
<View style={[styles.dFlex, styles.flexRow, styles.flexShrink1]}>
<RenderHTML html={messageContent} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/ReportActionItem/MoneyRequestPreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ function MoneyRequestPreview(props) {
}

let message = props.translate('iou.cash');
if (ReportUtils.isControlPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) {
if (ReportUtils.isGroupPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) {
message += ` • ${props.translate('iou.approved')}`;
} else if (props.iouReport.isWaitingOnBankAccount) {
message += ` • ${props.translate('iou.pending')}`;
Expand Down
4 changes: 3 additions & 1 deletion src/components/ReportActionItem/MoneyRequestView.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
if (!isDistanceRequest) {
amountDescription += ` • ${translate('iou.cash')}`;
}
if (isCancelled) {
if (ReportUtils.isReportApproved(report)) {
amountDescription += ` • ${translate('iou.approved')}`;
} else if (isCancelled) {
amountDescription += ` • ${translate('iou.canceled')}`;
} else if (isSettled) {
amountDescription += ` • ${translate('iou.settledExpensify')}`;
Expand Down
46 changes: 39 additions & 7 deletions src/components/ReportActionItem/ReportPreview.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React from 'react';
import React, {useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
Expand Down Expand Up @@ -47,6 +47,18 @@ const propTypes = {
/** The report's policyID, used for Onyx subscription */
policyID: PropTypes.string.isRequired,

/** The policy tied to the money request report */
policy: PropTypes.shape({
/** Name of the policy */
name: PropTypes.string,

/** Type of the policy */
type: PropTypes.string,

/** The role of the current user in the policy */
role: PropTypes.string,
}),

/* Onyx Props */
/** chatReport associated with iouReport */
chatReport: reportPropTypes,
Expand Down Expand Up @@ -101,6 +113,7 @@ const defaultProps = {
accountID: null,
},
isWhisper: false,
policy: {},
};

function ReportPreview(props) {
Expand All @@ -111,14 +124,16 @@ function ReportPreview(props) {
const managerID = props.iouReport.managerID || 0;
const isCurrentUserManager = managerID === lodashGet(props.session, 'accountID');
const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(props.iouReport);
const policyType = lodashGet(props.policy, 'type');

const iouSettled = ReportUtils.isSettled(props.iouReportID);
const iouCanceled = ReportUtils.isArchivedRoom(props.chatReport);
const numberOfRequests = ReportActionUtils.getNumberOfMoneyRequests(props.action);
const moneyRequestComment = lodashGet(props.action, 'childLastMoneyRequestComment', '');
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.chatReport);
const isDraftExpenseReport = isPolicyExpenseChat && ReportUtils.isDraftExpenseReport(props.iouReport);

const isApproved = ReportUtils.isReportApproved(props.iouReport);
const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(props.iouReport);
const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(props.iouReportID);
const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length;
const hasReceipts = transactionsWithReceipts.length > 0;
Expand Down Expand Up @@ -172,8 +187,9 @@ function ReportPreview(props) {
if (isScanning) {
return props.translate('common.receipt');
}
if (ReportUtils.isControlPolicyExpenseChat(props.chatReport) && ReportUtils.isReportApproved(props.iouReport)) {
return props.translate('iou.managerApproved', {manager: ReportUtils.getDisplayNameForParticipant(managerID, true)});
const payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
if (isApproved) {
return props.translate('iou.managerApproved', {manager: payerOrApproverName});
}
const managerName = isPolicyExpenseChat ? ReportUtils.getPolicyName(props.chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
let paymentVerb = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes';
Expand All @@ -184,10 +200,24 @@ function ReportPreview(props) {
};

const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport);
const shouldShowSettlementButton = ReportUtils.isControlPolicyExpenseChat(props.chatReport)
? props.policy.role === CONST.POLICY.ROLE.ADMIN && ReportUtils.isReportApproved(props.iouReport) && !iouSettled && !iouCanceled
: !_.isEmpty(props.iouReport) && isCurrentUserManager && !isDraftExpenseReport && !iouSettled && !iouCanceled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0;

const isGroupPolicy = ReportUtils.isGroupPolicyExpenseChat(props.chatReport);
const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(props.policy, 'role') === CONST.POLICY.ROLE.ADMIN;
const isPayer = isGroupPolicy
? // In a group policy, the admin approver can pay the report directly by skipping the approval step
isPolicyAdmin && (isApproved || isCurrentUserManager)
: isPolicyAdmin || (isMoneyRequestReport && isCurrentUserManager);
const shouldShowPayButton = useMemo(
() => isPayer && !isDraftExpenseReport && !iouSettled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled,
[isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, props.iouReport],
);
const shouldShowApproveButton = useMemo(() => {
if (!isGroupPolicy) {
return false;
}
return isCurrentUserManager && !isDraftExpenseReport && !isApproved && !iouSettled;
}, [isGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]);
const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton;
return (
<View style={[styles.chatItemMessage, ...props.containerStyles]}>
<PressableWithoutFeedback
Expand Down Expand Up @@ -251,6 +281,8 @@ function ReportPreview(props) {
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
style={[styles.mt3]}
kycWallAnchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
Expand Down
32 changes: 31 additions & 1 deletion src/components/SettlementButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import compose from '@libs/compose';
import * as ReportUtils from '@libs/ReportUtils';
import iouReportPropTypes from '@pages/iouReportPropTypes';
import * as BankAccounts from '@userActions/BankAccounts';
import * as IOU from '@userActions/IOU';
import * as PaymentMethods from '@userActions/PaymentMethods';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -36,6 +37,12 @@ const propTypes = {
/** The route to redirect if user does not have a payment method setup */
enablePaymentsRoute: PropTypes.string.isRequired,

/** Should we show the approve button? */
shouldHidePaymentOptions: PropTypes.bool,

/** Should we show the payment options? */
shouldShowApproveButton: PropTypes.bool,

/** The last payment method used per policy */
nvp_lastPaymentMethod: PropTypes.objectOf(PropTypes.string),

Expand Down Expand Up @@ -89,6 +96,8 @@ const defaultProps = {
// hook from being recreated unnecessarily, hence the use of CONST.EMPTY_ARRAY and CONST.EMPTY_OBJECT
iouReport: CONST.EMPTY_OBJECT,
nvp_lastPaymentMethod: CONST.EMPTY_OBJECT,
shouldHidePaymentOptions: false,
shouldShowApproveButton: false,
style: [],
policyID: '',
formattedAmount: '',
Expand Down Expand Up @@ -120,6 +129,8 @@ function SettlementButton({
onPress,
pressOnEnter,
policyID,
shouldHidePaymentOptions,
shouldShowApproveButton,
style,
}) {
const {translate} = useLocalize();
Expand Down Expand Up @@ -149,8 +160,18 @@ function SettlementButton({
value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
},
};
const approveButtonOption = {
text: translate('iou.approve'),
icon: Expensicons.ThumbsUp,
value: CONST.IOU.REPORT_ACTION_TYPE.APPROVE,
};
const canUseWallet = !isExpenseReport && currency === CONST.CURRENCY.USD;

// Only show the Approve button if the user cannot pay the request
if (shouldHidePaymentOptions && shouldShowApproveButton) {
return [approveButtonOption];
}

// To achieve the one tap pay experience we need to choose the correct payment type as default,
// if user already paid for some request or expense, let's use the last payment method or use default.
const paymentMethod = nvp_lastPaymentMethod[policyID] || '';
Expand All @@ -162,12 +183,16 @@ function SettlementButton({
}
buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.ELSEWHERE]);

if (shouldShowApproveButton) {
buttonOptions.push(approveButtonOption);
}

// Put the preferred payment method to the front of the array so its shown as default
if (paymentMethod) {
return _.sortBy(buttonOptions, (method) => (method.value === paymentMethod ? 0 : 1));
}
return buttonOptions;
}, [currency, formattedAmount, iouReport, nvp_lastPaymentMethod, policyID, translate]);
}, [currency, formattedAmount, iouReport, nvp_lastPaymentMethod, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton]);

const selectPaymentType = (event, iouPaymentType, triggerKYCFlow) => {
if (iouPaymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || iouPaymentType === CONST.IOU.PAYMENT_TYPE.VBBA) {
Expand All @@ -176,6 +201,11 @@ function SettlementButton({
return;
}

if (iouPaymentType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
IOU.approveMoneyRequest(iouReport);
return;
}

onPress(iouPaymentType);
};

Expand Down
Loading

0 comments on commit f454dd1

Please sign in to comment.