Skip to content

Commit

Permalink
Merge pull request #49007 from fabioh8010/feature/combined-expense-fl…
Browse files Browse the repository at this point in the history
…ow-48787

[A/B Testing - Create Expense] Implement Create Expense Flow under hidden Beta
  • Loading branch information
grgia authored Oct 1, 2024
2 parents 12e2808 + fdf3949 commit 1ff3994
Show file tree
Hide file tree
Showing 17 changed files with 178 additions and 71 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2059,6 +2059,7 @@ const CONST = {
INVOICE: 'invoice',
SUBMIT: 'submit',
TRACK: 'track',
CREATE: 'create',
},
REQUEST_TYPE: {
DISTANCE: 'distance',
Expand Down
5 changes: 4 additions & 1 deletion src/components/ReportWelcomeText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy);
const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, policy, participantAccountIDs);
const additionalText = moneyRequestOptions
.filter((item): item is Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.INVOICE> => item !== CONST.IOU.TYPE.INVOICE)
.filter(
(item): item is Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.CREATE | typeof CONST.IOU.TYPE.INVOICE> =>
item !== CONST.IOU.TYPE.INVOICE,
)
.map((item) => translate(`reportActionsView.iouTypes.${item}`))
.join(', ');
const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy);
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ const translations = {
share: 'Share',
participants: 'Participants',
submitExpense: 'Submit expense',
createExpense: 'Create expense',
trackExpense: 'Track expense',
pay: 'Pay',
cancelPayment: 'Cancel payment',
Expand Down Expand Up @@ -1019,6 +1020,7 @@ const translations = {
bookingPendingDescription: "This booking is pending because it hasn't been paid yet.",
bookingArchived: 'This booking is archived',
bookingArchivedDescription: 'This booking is archived because the trip date has passed. Add an expense for the final amount if needed.',
justTrackIt: 'Just track it (don’t submit it)',
},
notificationPreferencesPage: {
header: 'Notification preferences',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@ const translations = {
share: 'Compartir',
participants: 'Participantes',
submitExpense: 'Presentar gasto',
createExpense: 'Crear gasto',
paySomeone: ({name}: PaySomeoneParams = {}) => `Pagar a ${name ?? 'alguien'}`,
trackExpense: 'Seguimiento de gastos',
pay: 'Pagar',
Expand Down Expand Up @@ -1013,6 +1014,7 @@ const translations = {
bookingPendingDescription: 'Esta reserva está pendiente porque aún no se ha pagado.',
bookingArchived: 'Esta reserva está archivada',
bookingArchivedDescription: 'Esta reserva está archivada porque la fecha del viaje ha pasado. Agregue un gasto por el monto final si es necesario.',
justTrackIt: 'Solo guardarlo (no enviarlo)',
},
notificationPreferencesPage: {
header: 'Preferencias de avisos',
Expand Down
10 changes: 1 addition & 9 deletions src/libs/IOUUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,9 @@ function isValidMoneyRequestType(iouType: string): boolean {
CONST.IOU.TYPE.PAY,
CONST.IOU.TYPE.TRACK,
CONST.IOU.TYPE.INVOICE,
CONST.IOU.TYPE.CREATE,
];
return moneyRequestType.includes(iouType);
}

/**
* Checks if the iou type is one of submit, pay, track, or split.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
function temporary_isValidMoneyRequestType(iouType: string): boolean {
const moneyRequestType: string[] = [CONST.IOU.TYPE.SUBMIT, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.PAY, CONST.IOU.TYPE.TRACK, CONST.IOU.TYPE.INVOICE];
return moneyRequestType.includes(iouType);
}

Expand Down Expand Up @@ -169,5 +162,4 @@ export {
isValidMoneyRequestType,
navigateToStartMoneyRequestStep,
updateIOUOwnerAndTotal,
temporary_isValidMoneyRequestType,
};
15 changes: 12 additions & 3 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6700,8 +6700,10 @@ function temporary_getMoneyRequestOptions(
report: OnyxEntry<Report>,
policy: OnyxEntry<Policy>,
reportParticipants: number[],
): Array<Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND>> {
return getMoneyRequestOptions(report, policy, reportParticipants, true) as Array<Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND>>;
): Array<Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.CREATE>> {
return getMoneyRequestOptions(report, policy, reportParticipants, true) as Array<
Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.CREATE>
>;
}

/**
Expand Down Expand Up @@ -6942,10 +6944,17 @@ function getReportOfflinePendingActionAndErrors(report: OnyxEntry<Report>): Repo
*/
function canCreateRequest(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>, iouType: ValueOf<typeof CONST.IOU.TYPE>): boolean {
const participantAccountIDs = Object.keys(report?.participants ?? {}).map(Number);

if (!canUserPerformWriteAction(report)) {
return false;
}
return getMoneyRequestOptions(report, policy, participantAccountIDs).includes(iouType);

const requestOptions = getMoneyRequestOptions(report, policy, participantAccountIDs);
if (Permissions.canUseCombinedTrackSubmit(allBetas ?? [])) {
requestOptions.push(CONST.IOU.TYPE.CREATE);
}

return requestOptions.includes(iouType);
}

function getWorkspaceChats(policyID: string, accountIDs: number[]): Array<OnyxEntry<Report>> {
Expand Down
2 changes: 2 additions & 0 deletions src/libs/getIconForAction/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const getIconForAction = (actionType: ValueOf<typeof CONST.IOU.TYPE>) => {
return Expensicons.Cash;
case CONST.IOU.TYPE.SPLIT:
return Expensicons.Transfer;
case CONST.IOU.TYPE.CREATE:
return Expensicons.Receipt;
default:
return Expensicons.MoneyCircle;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';

type MoneyRequestOptions = Record<Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND>, PopoverMenuItem>;
type MoneyRequestOptions = Record<Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.CREATE>, PopoverMenuItem>;

type AttachmentPickerWithMenuItemsProps = {
/** The report currently being looked at */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {useOnyx, withOnyx} from 'react-native-onyx';
import type {SvgProps} from 'react-native-svg';
import FloatingActionButton from '@components/FloatingActionButton';
import * as Expensicons from '@components/Icon/Expensicons';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import PopoverMenu from '@components/PopoverMenu';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
Expand Down Expand Up @@ -186,7 +187,7 @@ function FloatingActionButtonAndPopover(
const prevIsFocused = usePrevious(isFocused);
const {isOffline} = useNetwork();

const {canUseSpotnanaTravel} = usePermissions();
const {canUseSpotnanaTravel, canUseCombinedTrackSubmit} = usePermissions();
const canSendInvoice = useMemo(() => PolicyUtils.canSendInvoice(allPolicies as OnyxCollection<OnyxTypes.Policy>, session?.email), [allPolicies, session?.email]);

const quickActionAvatars = useMemo(() => {
Expand Down Expand Up @@ -348,9 +349,70 @@ function FloatingActionButtonAndPopover(
showCreateMenu();
}
};

// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
const selfDMReportID = useMemo(() => ReportUtils.findSelfDMReportID(), [isLoading]);

const expenseMenuItems = useMemo((): PopoverMenuItem[] => {
if (canUseCombinedTrackSubmit) {
return [
{
icon: getIconForAction(CONST.IOU.TYPE.CREATE),
text: translate('iou.createExpense'),
onSelected: () =>
interceptAnonymousUser(() =>
IOU.startMoneyRequest(
CONST.IOU.TYPE.CREATE,
// When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used
// for all of the routes in the creation flow.
ReportUtils.generateReportID(),
),
),
},
];
}

return [
...(selfDMReportID
? [
{
icon: getIconForAction(CONST.IOU.TYPE.TRACK),
text: translate('iou.trackExpense'),
onSelected: () => {
interceptAnonymousUser(() =>
IOU.startMoneyRequest(
CONST.IOU.TYPE.TRACK,
// When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID.
// If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(),
),
);
if (!hasSeenTrackTraining && !isOffline) {
setTimeout(() => {
Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL);
}, CONST.ANIMATED_TRANSITION);
}
},
},
]
: []),
{
icon: getIconForAction(CONST.IOU.TYPE.REQUEST),
text: translate('iou.submitExpense'),
onSelected: () =>
interceptAnonymousUser(() =>
IOU.startMoneyRequest(
CONST.IOU.TYPE.SUBMIT,
// When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used
// for all of the routes in the creation flow.
ReportUtils.generateReportID(),
),
),
},
];
}, [canUseCombinedTrackSubmit, translate, selfDMReportID, hasSeenTrackTraining, isOffline]);

return (
<View style={styles.flexGrow1}>
<PopoverMenu
Expand All @@ -365,43 +427,7 @@ function FloatingActionButtonAndPopover(
text: translate('sidebarScreen.fabNewChat'),
onSelected: () => interceptAnonymousUser(Report.startNewChat),
},
...(selfDMReportID
? [
{
icon: getIconForAction(CONST.IOU.TYPE.TRACK),
text: translate('iou.trackExpense'),
onSelected: () => {
interceptAnonymousUser(() =>
IOU.startMoneyRequest(
CONST.IOU.TYPE.TRACK,
// When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID.
// If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(),
),
);
if (!hasSeenTrackTraining && !isOffline) {
setTimeout(() => {
Navigation.navigate(ROUTES.TRACK_TRAINING_MODAL);
}, CONST.ANIMATED_TRANSITION);
}
},
},
]
: []),
{
icon: getIconForAction(CONST.IOU.TYPE.REQUEST),
text: translate('iou.submitExpense'),
onSelected: () =>
interceptAnonymousUser(() =>
IOU.startMoneyRequest(
CONST.IOU.TYPE.SUBMIT,
// When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used
// for all of the routes in the creation flow.
ReportUtils.generateReportID(),
),
),
},
...expenseMenuItems,
...(canSendInvoice
? [
{
Expand Down
6 changes: 4 additions & 2 deletions src/pages/iou/request/IOURequestStartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ function IOURequestStartPage({
[CONST.IOU.TYPE.SPLIT]: translate('iou.splitExpense'),
[CONST.IOU.TYPE.TRACK]: translate('iou.trackExpense'),
[CONST.IOU.TYPE.INVOICE]: translate('workspace.invoices.sendInvoice'),
[CONST.IOU.TYPE.CREATE]: translate('iou.createExpense'),
};
const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction));
const {canUseP2PDistanceRequests} = usePermissions(iouType);
const {canUseP2PDistanceRequests, canUseCombinedTrackSubmit} = usePermissions(iouType);
const isFromGlobalCreate = isEmptyObject(report?.reportID);

// Clear out the temporary expense if the reportID in the URL has changed from the transaction's reportID
Expand All @@ -69,7 +70,8 @@ function IOURequestStartPage({

const isExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isExpenseReport = ReportUtils.isExpenseReport(report);
const shouldDisplayDistanceRequest = !!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.SPLIT);
const shouldDisplayDistanceRequest =
!!canUseCombinedTrackSubmit || !!canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.SPLIT);

const navigateBack = () => {
Navigation.closeRHPFlow();
Expand Down
40 changes: 37 additions & 3 deletions src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import EmptySelectionListContent from '@components/EmptySelectionListContent';
import FormHelpMessage from '@components/FormHelpMessage';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import {usePersonalDetails} from '@components/OnyxProvider';
import {useOptionsList} from '@components/OptionListContextProvider';
import ReferralProgramCTA from '@components/ReferralProgramCTA';
Expand Down Expand Up @@ -41,6 +43,9 @@ type MoneyRequestParticipantsSelectorProps = {
/** Callback to add participants in MoneyRequestModal */
onParticipantsAdded: (value: Participant[]) => void;

/** Callback to navigate to Track Expense confirmation flow */
onTrackExpensePress?: () => void;

/** Selected participants from MoneyRequestModal with login */
participants?: Participant[] | typeof CONST.EMPTY_ARRAY;

Expand All @@ -52,9 +57,21 @@ type MoneyRequestParticipantsSelectorProps = {

/** The action of the IOU, i.e. create, split, move */
action: IOUAction;

/** Whether we should display the Track Expense button at the top of the participants list */
shouldDisplayTrackExpenseButton?: boolean;
};

function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onFinish, onParticipantsAdded, iouType, iouRequestType, action}: MoneyRequestParticipantsSelectorProps) {
function MoneyRequestParticipantsSelector({
participants = CONST.EMPTY_ARRAY,
onTrackExpensePress,
onFinish,
onParticipantsAdded,
iouType,
iouRequestType,
action,
shouldDisplayTrackExpenseButton,
}: MoneyRequestParticipantsSelectorProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
Expand Down Expand Up @@ -105,9 +122,9 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF
participants as Participant[],
CONST.EXPENSIFY_EMAILS,

// If we are using this component in the "Submit expense" flow then we pass the includeOwnedWorkspaceChats argument so that the current user
// If we are using this component in the "Submit expense" or the combined submit/track flow then we pass the includeOwnedWorkspaceChats argument so that the current user
// sees the option to submit an expense from their admin on their own Workspace Chat.
(iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.SPLIT) && action !== CONST.IOU.ACTION.SUBMIT,
(iouType === CONST.IOU.TYPE.SUBMIT || iouType === CONST.IOU.TYPE.CREATE || iouType === CONST.IOU.TYPE.SPLIT) && action !== CONST.IOU.ACTION.SUBMIT,

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && !isCategorizeOrShareAction,
Expand Down Expand Up @@ -364,6 +381,22 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF

const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !shouldShowListEmptyContent;

const headerContent = useMemo(() => {
if (!shouldDisplayTrackExpenseButton) {
return;
}

// We only display the track expense button if the user is coming from the combined submit/track flow.
return (
<MenuItem
title={translate('iou.justTrackIt')}
shouldShowRightIcon
icon={Expensicons.Coins}
onPress={onTrackExpensePress}
/>
);
}, [shouldDisplayTrackExpenseButton, translate, onTrackExpensePress]);

const footerContent = useMemo(() => {
if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) {
return;
Expand Down Expand Up @@ -449,6 +482,7 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
onSelectRow={onSelectRow}
shouldSingleExecuteRowSelect
headerContent={headerContent}
footerContent={footerContent}
listEmptyContent={<EmptySelectionListContent contentType={iouType} />}
headerMessage={header}
Expand Down
Loading

0 comments on commit 1ff3994

Please sign in to comment.