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

Feature: [P2P Distance] Enable P2P/splits in App #37185

Merged
merged 17 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,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 @@ -1403,6 +1404,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 @@ -126,6 +126,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 @@ -520,6 +523,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 @@ -720,13 +721,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 @@ -242,11 +242,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 @@ -680,13 +681,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
Loading