Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WAVE7] Hold Requests: Approving/Paying expense reports with held requests #33124

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
3f4df08
Add reason interstatial
BartoszGrajdek Nov 22, 2023
faebe01
Merge remote-tracking branch 'origin/main' into hold-requests/money-r…
BartoszGrajdek Nov 22, 2023
f4297cd
Hold banner
BartoszGrajdek Nov 23, 2023
317f5fc
Add new translations
BartoszGrajdek Dec 5, 2023
da7e0a6
Change Header styles
BartoszGrajdek Dec 5, 2023
d38c0f6
Add full boolean to IOU requests
BartoszGrajdek Dec 5, 2023
818a841
Mock Report & Transaction Utils
BartoszGrajdek Dec 5, 2023
ed3fe5e
Add Offline Support
BartoszGrajdek Dec 14, 2023
275e17b
merge main
BartoszGrajdek Dec 14, 2023
7301183
Refactor utility functions
BartoszGrajdek Dec 14, 2023
f28bd09
fix: prettier
BartoszGrajdek Dec 14, 2023
e237009
Remove redundant import
BartoszGrajdek Dec 14, 2023
4e36be7
Merge remote-tracking branch 'origin/main' into hold-requests/money-r…
BartoszGrajdek Dec 14, 2023
38f28b5
fix: resolve merge conflicts
BartoszGrajdek Dec 18, 2023
7fefad2
fix: imports
BartoszGrajdek Dec 18, 2023
aa63896
fix: remove merge conflict
BartoszGrajdek Dec 18, 2023
cc5b1a0
Resolve merge conflict
BartoszGrajdek Dec 18, 2023
adea766
fix: prettier
BartoszGrajdek Dec 18, 2023
7067e21
fix: typecheck
BartoszGrajdek Dec 18, 2023
eb74222
Refactor Hold Feature
BartoszGrajdek Dec 18, 2023
19aa76d
Remove comment
BartoszGrajdek Dec 18, 2023
32e7c7e
imports fixes
BartoszGrajdek Dec 18, 2023
47c7c8d
feat: add offline support & system notifications
BartoszGrajdek Dec 29, 2023
c56681c
Resolve merge conflicts
BartoszGrajdek Dec 29, 2023
2fee6b7
fix: rename API call
BartoszGrajdek Jan 2, 2024
7033f38
refactor: migrate new files & new functions to TS
BartoszGrajdek Jan 2, 2024
04636a8
fix: change names
BartoszGrajdek Jan 2, 2024
b81c5df
fix: handle a case where every request is on HOLD
BartoszGrajdek Jan 2, 2024
20f568c
fix: order of system messages created date correction
BartoszGrajdek Jan 2, 2024
34e2d8c
fix: remove console logs
BartoszGrajdek Jan 2, 2024
d31c2a8
chore: resolve merge conflicts
BartoszGrajdek Jan 3, 2024
9eda512
chore: resolve merge conflicts
BartoszGrajdek Jan 10, 2024
beb1dc8
feat: add individual money request changes
BartoszGrajdek Jan 10, 2024
01e1bdb
feat: add offline support for partial payments/approvals
BartoszGrajdek Jan 18, 2024
7bd074d
chore: resolve merge conflicts
BartoszGrajdek Feb 4, 2024
3e90c40
fix: update PR with individual money request changes
BartoszGrajdek Feb 5, 2024
2574741
fix: lint
BartoszGrajdek Feb 5, 2024
75741ae
fix: merge problems
BartoszGrajdek Feb 5, 2024
3c1edf5
fix: lint
BartoszGrajdek Feb 5, 2024
14a7b65
chore: resolve merge conflicts
BartoszGrajdek Feb 5, 2024
c080092
chore: resolve merge conflicts
BartoszGrajdek Feb 19, 2024
9c4c695
fix: resolve merge conflicts
BartoszGrajdek Feb 19, 2024
914443e
fix: typecheck fixes
BartoszGrajdek Feb 19, 2024
d9a8fec
fix: remove JS file
BartoszGrajdek Feb 19, 2024
4734744
fix: lint
BartoszGrajdek Feb 19, 2024
7428752
fix: imports & double negation
BartoszGrajdek Feb 19, 2024
68aa153
fix: remove Form.ts
BartoszGrajdek Feb 19, 2024
46e626b
Merge remote-tracking branch 'origin/main' into hold-requests/money-r…
BartoszGrajdek Feb 20, 2024
5c44e41
Merge remote-tracking branch 'origin/main' into hold-requests/money-r…
BartoszGrajdek Feb 20, 2024
37311c3
feat: change how reports with held requests are paid/approved optimis…
BartoszGrajdek Feb 20, 2024
69839f1
fix: partial approvals modal not being shown
BartoszGrajdek Feb 21, 2024
5c535dd
chore: resolve merge conflicts
BartoszGrajdek Feb 21, 2024
a8a414f
feat: add handling for unheldTotal
BartoszGrajdek Mar 1, 2024
3cd3849
chore: lint
BartoszGrajdek Mar 1, 2024
7f691ff
chore: resolve merge conflicts
BartoszGrajdek Mar 1, 2024
071ca7b
fix: offline support for system messages
BartoszGrajdek Mar 6, 2024
d35f1cb
feat: hide checkmark for partially paid requests
BartoszGrajdek Mar 6, 2024
a86501a
chore: resolve merge conflicts
BartoszGrajdek Mar 21, 2024
54d293b
fix: amount displayed & pay/approve button
BartoszGrajdek Mar 21, 2024
9780889
chore: resolve merge conflicts
BartoszGrajdek Mar 27, 2024
cbb1a7b
fix: review suggestions & small bugs
BartoszGrajdek Mar 28, 2024
6f99630
fix: change comments
BartoszGrajdek Mar 28, 2024
e538272
chore: resolve merge conflicts
BartoszGrajdek Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions src/components/DecisionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import Button from './Button';
import Header from './Header';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import Modal from './Modal';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import Text from './Text';
import Tooltip from './Tooltip';

type DecisionModalProps = {
/** Title describing purpose of modal */
title: string;

/** Modal subtitle/description */
prompt?: string;

/** Text content used in first button */
firstOptionText?: string;

/** Text content used in second button */
secondOptionText: string;

/** onSubmit callback fired after clicking on first button */
onFirstOptionSubmit: () => void;

/** onSubmit callback fired after clicking on second button */
onSecondOptionSubmit: () => void;

/** Is the window width narrow, like on a mobile device? */
isSmallScreenWidth: boolean;

/** Callback for closing modal */
onClose: () => void;

/** Whether modal is visible */
isVisible: boolean;
};

function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, onFirstOptionSubmit, onSecondOptionSubmit, isSmallScreenWidth, onClose, isVisible}: DecisionModalProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();

return (
<Modal
onClose={onClose}
isVisible={isVisible}
type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
>
<View style={[styles.m5]}>
<View>
<View style={[styles.flexRow, styles.mb4]}>
<Header
title={title}
containerStyles={[styles.alignItemsCenter]}
/>
<Tooltip text={translate('common.close')}>
<PressableWithoutFeedback
onPress={onClose}
style={[styles.touchableButtonImage]}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('common.close')}
>
<Icon
src={Expensicons.Close}
fill={theme.icon}
/>
</PressableWithoutFeedback>
</Tooltip>
</View>

<Text>{prompt}</Text>
</View>
{firstOptionText && (
<Button
success
style={[styles.mt4]}
onPress={onFirstOptionSubmit}
pressOnEnter
text={firstOptionText}
/>
)}
<Button
style={[styles.mt3, styles.noSelect]}
onPress={onSecondOptionSubmit}
text={secondOptionText}
/>
</View>
</Modal>
);
}

DecisionModal.displayName = 'DecisionModal';

export default DecisionModal;
9 changes: 6 additions & 3 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {ReactNode} from 'react';
import React from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import EnvironmentBadge from './EnvironmentBadge';
Expand All @@ -18,12 +18,15 @@ type HeaderProps = {

/** Additional text styles */
textStyles?: StyleProp<TextStyle>;

/** Additional header container styles */
containerStyles?: StyleProp<ViewStyle>;
};

function Header({title = '', subtitle = '', textStyles = [], shouldShowEnvironmentBadge = false}: HeaderProps) {
function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false}: HeaderProps) {
const styles = useThemeStyles();
return (
<View style={[styles.flex1, styles.flexRow]}>
<View style={[styles.flex1, styles.flexRow, containerStyles]}>
<View style={styles.mw100}>
{typeof title === 'string'
? Boolean(title) && (
Expand Down
57 changes: 48 additions & 9 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import * as Expensicons from './Icon/Expensicons';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
import SettlementButton from './SettlementButton';

type PaymentType = DeepValueOf<typeof CONST.IOU.PAYMENT_TYPE>;

type MoneyReportHeaderOnyxProps = {
/** The chat report this report is linked to */
chatReport: OnyxEntry<OnyxTypes.Report>;
Expand All @@ -49,6 +48,9 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport);
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [paymentType, setPaymentType] = useState<PaymentMethodType>();
const [requestType, setRequestType] = useState<'pay' | 'approve'>();
const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport);
const policyType = policy?.type;
const isPayer = ReportUtils.isPayer(session, moneyRequestReport);
Expand Down Expand Up @@ -78,8 +80,32 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency);
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport);
const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@BartoszGrajdek Is there a reason that the canAllowSettlement condition is required here?

const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && isSmallScreenWidth);

const confirmPayment = (type?: PaymentMethodType | undefined) => {
if (!type) {
return;
}
setPaymentType(type);
setRequestType('pay');
if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) {
setIsHoldMenuVisible(true);
} else if (chatReport) {
IOU.payMoneyRequest(type, chatReport, moneyRequestReport, false);
Copy link
Contributor

@alitoshmatov alitoshmatov Apr 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true value should have been passed since this function is handling full payment. ref: #39357

}
};

const confirmApproval = () => {
setRequestType('approve');
if (ReportUtils.hasHeldExpenses(moneyRequestReport.reportID)) {
setIsHoldMenuVisible(true);
} else {
IOU.approveMoneyRequest(moneyRequestReport, true);
}
};

// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(
() => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled,
Expand Down Expand Up @@ -115,18 +141,18 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
<View style={styles.pv2}>
<SettlementButton
currency={moneyRequestReport.currency}
confirmApproval={confirmApproval}
policyID={moneyRequestReport.policyID}
chatReportID={chatReport?.reportID}
iouReport={moneyRequestReport}
// @ts-expect-error TODO: Remove this once IOU (https://github.com/Expensify/App/issues/24926) is migrated to TypeScript.
onPress={(paymentType: PaymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
onPress={confirmPayment}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
shouldDisableApproveButton={shouldDisableApproveButton}
style={[styles.pv2]}
formattedAmount={formattedAmount}
formattedAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? displayedAmount : ''}
isDisabled={!canAllowSettlement}
/>
</View>
Expand All @@ -149,17 +175,17 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
<View style={[styles.ph5, styles.pb2]}>
<SettlementButton
currency={moneyRequestReport.currency}
confirmApproval={confirmApproval}
policyID={moneyRequestReport.policyID}
chatReportID={moneyRequestReport.chatReportID}
iouReport={moneyRequestReport}
// @ts-expect-error TODO: Remove this once IOU (https://github.com/Expensify/App/issues/24926) is migrated to TypeScript.
onPress={(paymentType: PaymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
onPress={confirmPayment}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
shouldShowApproveButton={shouldShowApproveButton}
formattedAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? displayedAmount : ''}
shouldDisableApproveButton={shouldDisableApproveButton}
formattedAmount={formattedAmount}
isDisabled={!canAllowSettlement}
/>
</View>
Expand All @@ -182,6 +208,19 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
</View>
)}
</View>
{isHoldMenuVisible && requestType !== undefined && (
<ProcessMoneyReportHoldMenu
nonHeldAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? nonHeldAmount : undefined}
requestType={requestType}
fullAmount={fullAmount}
isSmallScreenWidth={isSmallScreenWidth}
onClose={() => setIsHoldMenuVisible(false)}
isVisible={isHoldMenuVisible}
paymentType={paymentType}
chatReport={chatReport}
moneyRequestReport={moneyRequestReport}
/>
)}
<ConfirmModal
title={translate('iou.cancelPayment')}
isVisible={isConfirmModalVisible}
Expand Down
78 changes: 78 additions & 0 deletions src/components/ProcessMoneyReportHoldMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import * as IOU from '@userActions/IOU';
import type * as OnyxTypes from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import DecisionModal from './DecisionModal';

type ProcessMoneyReportHoldMenuProps = {
/** The chat report this report is linked to */
chatReport: OnyxEntry<OnyxTypes.Report>;

/** Full amount of expense report to pay */
fullAmount: string;

/** Is the window width narrow, like on a mobile device? */
isSmallScreenWidth: boolean;

/** Whether modal is visible */
isVisible: boolean;

/** The report currently being looked at */
moneyRequestReport: OnyxTypes.Report;

/** Not held amount of expense report */
nonHeldAmount?: string;

/** Callback for closing modal */
onClose: () => void;

/** Type of payment */
paymentType?: PaymentMethodType;

/** Type of action handled */
requestType?: 'pay' | 'approve';
};

function ProcessMoneyReportHoldMenu({
requestType,
nonHeldAmount,
fullAmount,
isSmallScreenWidth = false,
onClose,
isVisible,
paymentType,
chatReport,
moneyRequestReport,
}: ProcessMoneyReportHoldMenuProps) {
const {translate} = useLocalize();
const isApprove = requestType === 'approve';

const onSubmit = (full: boolean) => {
if (isApprove) {
IOU.approveMoneyRequest(moneyRequestReport, full);
} else if (chatReport && paymentType) {
IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport, full);
}
onClose();
};

return (
<DecisionModal
title={translate(isApprove ? 'iou.confirmApprove' : 'iou.confirmPay')}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB: just making a note for later cleanup- we can probably move these string constants to https://github.com/Expensify/App/blob/main/src/CONST.ts

onClose={onClose}
isVisible={isVisible}
prompt={translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount')}
firstOptionText={nonHeldAmount ? `${translate(isApprove ? 'iou.approveOnly' : 'iou.payOnly')} ${nonHeldAmount}` : undefined}
secondOptionText={`${translate(isApprove ? 'iou.approve' : 'iou.pay')} ${fullAmount}`}
onFirstOptionSubmit={() => onSubmit(false)}
onSecondOptionSubmit={() => onSubmit(true)}
isSmallScreenWidth={isSmallScreenWidth}
/>
);
}

ProcessMoneyReportHoldMenu.displayName = 'ProcessMoneyReportHoldMenu';

export default ProcessMoneyReportHoldMenu;
3 changes: 2 additions & 1 deletion src/components/ReportActionItem/MoneyReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReport
const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, report.currency);
const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, report.currency);
const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, report.currency);
const isPartiallyPaid = Boolean(report?.pendingFields?.partial);

const subAmountTextStyles: StyleProp<TextStyle> = [
styles.taskTitleMenuItem,
Expand Down Expand Up @@ -109,7 +110,7 @@ function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReport
</Text>
</View>
<View style={[styles.flexRow, styles.justifyContentCenter]}>
{isSettled && (
{isSettled && !isPartiallyPaid && (
<View style={[styles.defaultCheckmarkWrapper, styles.mh2]}>
<Icon
src={Expensicons.Checkmark}
Expand Down
Loading
Loading