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 10 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 @@ -307,6 +307,7 @@ const CONST = {
BETA_COMMENT_LINKING: 'commentLinking',
VIOLATIONS: 'violations',
REPORT_FIELDS: 'reportFields',
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down Expand Up @@ -1395,6 +1396,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 @@ -519,6 +522,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
6 changes: 3 additions & 3 deletions src/components/MoneyRequestConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ 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;
Expand Down Expand Up @@ -719,13 +719,13 @@ function MoneyRequestConfirmationList(props) {
)}
{props.isDistanceRequest && (
<MenuItemWithTopDescription
shouldShowRightIcon={!props.isReadOnly && isTypeRequest}
shouldShowRightIcon={!props.isReadOnly && (canUseP2PDistanceRequests || isTypeRequest)}
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}
disabled={didConfirm || !(canUseP2PDistanceRequests || isTypeRequest)}
interactive={!props.isReadOnly}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ 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;
Expand Down Expand Up @@ -679,13 +679,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
item: (
<MenuItemWithTopDescription
key={translate('common.distance')}
shouldShowRightIcon={!isReadOnly && isTypeRequest}
shouldShowRightIcon={!isReadOnly && (canUseP2PDistanceRequests || isTypeRequest)}
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}
disabled={didConfirm || !(canUseP2PDistanceRequests || isTypeRequest)}
interactive={!isReadOnly}
/>
),
Expand Down
25 changes: 24 additions & 1 deletion 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 = {
rateID?: string;
rate?: number;
currency?: string;
unit: Unit;
Expand Down Expand Up @@ -38,6 +39,7 @@ function getDefaultMileageRate(policy: OnyxEntry<Policy>): DefaultMileageRate |
}

return {
rateID: 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 Down Expand Up @@ -124,4 +147,4 @@ function getDistanceRequestAmount(distance: number, unit: Unit, rate: number): n
return Math.round(roundedDistance * rate);
}

export default {getDefaultMileageRate, getDistanceMerchant, getDistanceRequestAmount};
export default {getDefaultMileageRate, getDistanceForDisplay, getDistanceMerchant, getDistanceRequestAmount};
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);
}

/**
* Link previews are temporarily disabled.
*/
Expand All @@ -40,4 +44,5 @@ export default {
canUseLinkPreviews,
canUseViolations,
canUseReportFields,
canUseP2PDistanceRequests,
};
19 changes: 18 additions & 1 deletion src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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 @@ -220,12 +221,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 @@ -239,6 +250,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 ?? '']?.customUnitRateID ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.rateID ?? '';
}
comment.customUnit = {customUnitRateID};
}

// Store the transaction in Onyx and mark it as not saved so it can be cleaned up later
Expand Down
8 changes: 4 additions & 4 deletions src/pages/iou/request/IOURequestStartPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ 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);
Expand All @@ -117,10 +117,10 @@ function IOURequestStartPage({
if (newIouType === previousIOURequestType) {
return;
}
IOU.initMoneyRequest(reportID, isFromGlobalCreate, newIouType);
IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, newIouType);
transactionRequestType.current = newIouType;
},
[previousIOURequestType, reportID, isFromGlobalCreate],
[policy, previousIOURequestType, reportID, 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 @@ -181,7 +179,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 @@ -256,7 +254,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
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 @@ -94,6 +95,7 @@ function MoneyRequestParticipantsSelector({
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 maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached);
Expand All @@ -113,8 +115,7 @@ function MoneyRequestParticipantsSelector({
// 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.
!isDistanceRequest,
canUseP2PDistanceRequests || !isDistanceRequest,
false,
{},
[],
Expand All @@ -123,15 +124,15 @@ function MoneyRequestParticipantsSelector({
[],
// 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
!isDistanceRequest,
canUseP2PDistanceRequests || !isDistanceRequest,
true,
);
return {
recentReports: chatOptions.recentReports,
personalDetails: chatOptions.personalDetails,
userToInvite: chatOptions.userToInvite,
};
}, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]);
}, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest, canUseP2PDistanceRequests]);

/**
* Returns the sections needed for the OptionsSelector
Expand Down Expand Up @@ -272,7 +273,7 @@ function MoneyRequestParticipantsSelector({
// the app from crashing on native when you try to do this, we'll going to show error message if you have a workspace and other participants
const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat);
const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant;
const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND;
const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && iouType !== CONST.IOU.TYPE.SEND;

const handleConfirmSelection = useCallback(() => {
if (shouldShowSplitBillErrorMessage) {
Expand Down
5 changes: 5 additions & 0 deletions src/types/onyx/LastSelectedDistanceRates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type {Rate} from './Policy';

type LastSelectedDistanceRates = Record<string, Rate>;

export default LastSelectedDistanceRates;
2 changes: 2 additions & 0 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type IntroSelected from './IntroSelected';
import type InvitedEmailsToAccountIDs from './InvitedEmailsToAccountIDs';
import type IOU from './IOU';
import type LastPaymentMethod from './LastPaymentMethod';
import type LastSelectedDistanceRates from './LastSelectedDistanceRates';
import type Locale from './Locale';
import type {LoginList} from './Login';
import type Login from './Login';
Expand Down Expand Up @@ -148,6 +149,7 @@ export type {
PolicyReportFields,
RecentlyUsedReportFields,
LastPaymentMethod,
LastSelectedDistanceRates,
InvitedEmailsToAccountIDs,
Log,
};
Loading