Skip to content

Commit

Permalink
Merge pull request #37185 from tienifr/fix/36984
Browse files Browse the repository at this point in the history
Feature: [P2P Distance] Enable P2P/splits in App
  • Loading branch information
neil-marcellini authored Mar 6, 2024
2 parents 44fc9e1 + 26c3979 commit 6c320c4
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 30 deletions.
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ const CONST = {
BETA_COMMENT_LINKING: 'commentLinking',
VIOLATIONS: 'violations',
REPORT_FIELDS: 'reportFields',
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission',
},
BUTTON_STATES: {
Expand Down Expand Up @@ -1414,6 +1415,7 @@ const CONST = {
MILEAGE_IRS_RATE: 0.655,
DEFAULT_RATE: 'Default Rate',
RATE_DECIMALS: 3,
FAKE_P2P_ID: '_FAKE_P2P_ID_',
},

TERMS: {
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ const ONYXKEYS = {
/** This NVP contains the choice that the user made on the engagement modal */
NVP_INTRO_SELECTED: 'introSelected',

/** The NVP with the last distance rate used per policy */
NVP_LAST_SELECTED_DISTANCE_RATES: 'lastSelectedDistanceRates',

/** Does this user have push notifications enabled for this device? */
PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled',

Expand Down Expand Up @@ -527,6 +530,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[];
[ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean;
[ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected;
[ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates;
[ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean;
[ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData;
[ONYXKEYS.IS_PLAID_DISABLED]: boolean;
Expand Down
8 changes: 5 additions & 3 deletions src/components/MoneyRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,11 +217,12 @@ function MoneyRequestConfirmationList(props) {
const {onSendMoney, onConfirm, onSelectParticipant} = props;
const {translate, toLocaleDigit} = useLocalize();
const transaction = props.transaction;
const {canUseViolations} = usePermissions();
const {canUseP2PDistanceRequests, canUseViolations} = usePermissions();

const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST;
const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT;
const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND;
const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isSplitBill);

const isSplitWithScan = isSplitBill && props.isScanRequest;

Expand Down Expand Up @@ -721,13 +722,14 @@ function MoneyRequestConfirmationList(props) {
)}
{props.isDistanceRequest && (
<MenuItemWithTopDescription
shouldShowRightIcon={!props.isReadOnly && isTypeRequest}
shouldShowRightIcon={!props.isReadOnly && canEditDistance}
title={props.iouMerchant}
description={translate('common.distance')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))}
disabled={didConfirm || !isTypeRequest}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={didConfirm || !canEditDistance}
interactive={!props.isReadOnly}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const theme = useTheme();
const styles = useThemeStyles();
const {translate, toLocaleDigit} = useLocalize();
const {canUseViolations} = usePermissions();
const {canUseP2PDistanceRequests, canUseViolations} = usePermissions();

const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT;
const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit);

const {unit, rate, currency} = mileageRate;
const distance = lodashGet(transaction, 'routes.route0.distance', 0);
Expand Down Expand Up @@ -689,13 +690,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
item: (
<MenuItemWithTopDescription
key={translate('common.distance')}
shouldShowRightIcon={!isReadOnly && isTypeRequest}
shouldShowRightIcon={!isReadOnly && canEditDistance}
title={iouMerchant}
description={translate('common.distance')}
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
disabled={didConfirm || !isTypeRequest}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={didConfirm || !canEditDistance}
interactive={!isReadOnly}
/>
),
Expand Down
29 changes: 25 additions & 4 deletions src/libs/DistanceRequestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as CurrencyUtils from './CurrencyUtils';
import * as PolicyUtils from './PolicyUtils';

type DefaultMileageRate = {
customUnitRateID?: string;
rate?: number;
currency?: string;
unit: Unit;
Expand Down Expand Up @@ -38,6 +39,7 @@ function getDefaultMileageRate(policy: OnyxEntry<Policy>): DefaultMileageRate |
}

return {
customUnitRateID: distanceRate.customUnitRateID,
rate: distanceRate.rate,
currency: distanceRate.currency,
unit: distanceUnit.attributes.unit,
Expand Down Expand Up @@ -76,6 +78,27 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string
return convertedDistance.toFixed(2);
}

/**
* @param hasRoute Whether the route exists for the distance request
* @param distanceInMeters Distance traveled
* @param unit Unit that should be used to display the distance
* @param rate Expensable amount allowed per unit
* @param translate Translate function
* @returns A string that describes the distance traveled
*/
function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, rate: number, translate: LocaleContextProps['translate']): string {
if (!hasRoute || !rate) {
return translate('iou.routePending');
}

const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit);
const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers');
const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer');
const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit;

return `${distanceInUnits} ${unitString}`;
}

/**
* @param hasRoute Whether the route exists for the distance request
* @param distanceInMeters Distance traveled
Expand All @@ -99,15 +122,13 @@ function getDistanceMerchant(
return translate('iou.routePending');
}

const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit);
const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers');
const formattedDistance = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate);
const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer');
const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit;
const ratePerUnit = PolicyUtils.getUnitRateValue({rate}, toLocaleDigit);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const currencySymbol = CurrencyUtils.getCurrencySymbol(currency) || `${currency} `;

return `${distanceInUnits} ${unitString} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`;
return `${formattedDistance} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ function canUseViolations(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas);
}

function canUseP2PDistanceRequests(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas);
}

function canUseWorkflowsDelayedSubmission(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas);
}
Expand All @@ -44,5 +48,6 @@ export default {
canUseLinkPreviews,
canUseViolations,
canUseReportFields,
canUseP2PDistanceRequests,
canUseWorkflowsDelayedSubmission,
};
19 changes: 18 additions & 1 deletion src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
import {WRITE_COMMANDS} from '@libs/API/types';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import * as IOUUtils from '@libs/IOUUtils';
Expand Down Expand Up @@ -222,12 +223,22 @@ Onyx.connect({
},
});

let lastSelectedDistanceRates: OnyxEntry<OnyxTypes.LastSelectedDistanceRates> = {};
Onyx.connect({
key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES,
callback: (value) => {
lastSelectedDistanceRates = value;
},
});

/**
* Initialize money request info
* @param reportID to attach the transaction to
* @param policy
* @param isFromGlobalCreate
* @param iouRequestType one of manual/scan/distance
*/
function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) {
function initMoneyRequest(reportID: string, policy: OnyxEntry<OnyxTypes.Policy>, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) {
// Generate a brand new transactionID
const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID;
// Disabling this line since currentDate can be an empty string
Expand All @@ -241,6 +252,12 @@ function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequ
waypoint0: {},
waypoint1: {},
};
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null;
let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID;
if (ReportUtils.isPolicyExpenseChat(report)) {
customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? '';
}
comment.customUnit = {customUnitRateID};
}

// Store the transaction in Onyx and mark it as not saved so it can be cleaned up later
Expand Down
4 changes: 3 additions & 1 deletion src/pages/iou/MoneyRequestSelectorPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import TabSelector from '@components/TabSelector/TabSelector';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
Expand Down Expand Up @@ -62,6 +63,7 @@ const defaultProps = {
function MoneyRequestSelectorPage(props) {
const styles = useThemeStyles();
const [isDraggingOver, setIsDraggingOver] = useState(false);
const {canUseP2PDistanceRequests} = usePermissions();

const iouType = lodashGet(props.route, 'params.iouType', '');
const reportID = lodashGet(props.route, 'params.reportID', '');
Expand All @@ -75,7 +77,7 @@ function MoneyRequestSelectorPage(props) {
const isFromGlobalCreate = !reportID;
const isExpenseChat = ReportUtils.isPolicyExpenseChat(props.report);
const isExpenseReport = ReportUtils.isExpenseReport(props.report);
const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate;
const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate;

const resetMoneyRequestInfo = () => {
const moneyRequestID = `${iouType}${reportID}`;
Expand Down
12 changes: 7 additions & 5 deletions src/pages/iou/request/IOURequestStartPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import TabSelector from '@components/TabSelector/TabSelector';
import transactionPropTypes from '@components/transactionPropTypes';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
Expand Down Expand Up @@ -80,6 +81,7 @@ function IOURequestStartPage({
};
const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction));
const previousIOURequestType = usePrevious(transactionRequestType.current);
const {canUseP2PDistanceRequests} = usePermissions();
const isFromGlobalCreate = _.isEmpty(report.reportID);

useFocusEffect(
Expand All @@ -102,12 +104,12 @@ function IOURequestStartPage({
if (transaction.reportID === reportID) {
return;
}
IOU.initMoneyRequest(reportID, isFromGlobalCreate, transactionRequestType.current);
}, [transaction, reportID, iouType, isFromGlobalCreate]);
IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, transactionRequestType.current);
}, [transaction, policy, reportID, iouType, isFromGlobalCreate]);

const isExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isExpenseReport = ReportUtils.isExpenseReport(report);
const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate;
const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate;

// Allow the user to create the request if we are creating the request in global menu or the report can create the request
const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType);
Expand All @@ -124,10 +126,10 @@ function IOURequestStartPage({
if (iouType === CONST.IOU.TYPE.SPLIT && transaction.isFromGlobalCreate) {
IOU.updateMoneyRequestTypeParams(navigation.getState().routes, CONST.IOU.TYPE.REQUEST, newIouType);
}
IOU.initMoneyRequest(reportID, isFromGlobalCreate, newIouType);
IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, newIouType);
transactionRequestType.current = newIouType;
},
[previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation, transaction.isFromGlobalCreate],
[policy, previousIOURequestType, reportID, isFromGlobalCreate, iouType, navigation, transaction.isFromGlobalCreate],
);

if (!transaction.transactionID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/UserListItem';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePermissions from '@hooks/usePermissions';
import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
Expand Down Expand Up @@ -90,6 +91,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST;
const {isOffline} = useNetwork();
const personalDetails = usePersonalDetails();
const {canUseP2PDistanceRequests} = usePermissions();

const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : '';

Expand Down Expand Up @@ -120,18 +122,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
// sees the option to request money from their admin on their own Workspace Chat.
iouType === CONST.IOU.TYPE.REQUEST,

// We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features.
iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
false,
{},
[],
false,
{},
[],

// We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now.
// This functionality is being built here: https://github.com/Expensify/App/issues/23291
iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE,
false,
);

Expand Down Expand Up @@ -182,7 +180,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
}

return [newSections, chatOptions];
}, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, translate]);
}, [didScreenTransitionEnd, reports, personalDetails, betas, searchTerm, participants, iouType, iouRequestType, maxParticipantsReached, canUseP2PDistanceRequests, translate]);

/**
* Adds a single participant to the request
Expand Down Expand Up @@ -257,7 +255,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
// 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 shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant;
const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE;
const isAllowedToSplit = canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE;

const handleConfirmSelection = useCallback(() => {
if (shouldShowSplitBillErrorMessage) {
Expand Down
Loading

0 comments on commit 6c320c4

Please sign in to comment.