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

[A/B Testing - Create Expense] Implement Create Expense Flow under hidden Beta #49007

Merged
merged 28 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3e0f77d
Add "Create Expense" item to Global Create menu
fabioh8010 Sep 10, 2024
96da7f1
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 11, 2024
ecbf217
Update getIconForAction() to account for create IOU type
fabioh8010 Sep 11, 2024
a04f4e4
Add create iou type to isValidMoneyRequestType() and delete temporary…
fabioh8010 Sep 11, 2024
80acc3b
Change canCreateRequest() to include create iou type if combined expe…
fabioh8010 Sep 11, 2024
dbe07eb
Add tab title for create iou type in IOURequestStartPage
fabioh8010 Sep 11, 2024
18f9585
Merge remote-tracking branch 'origin/main' into feature/combined-expe…
fabioh8010 Sep 12, 2024
67cb211
Navigate the user to participants page if they are using the combined…
fabioh8010 Sep 12, 2024
63aa38b
Add Track expense button to IOU participants page
fabioh8010 Sep 12, 2024
6b17906
Fix TS error
fabioh8010 Sep 12, 2024
093b947
Merge remote-tracking branch 'origin/main' into feature/combined-expe…
fabioh8010 Sep 13, 2024
d7f4032
Fix TS
fabioh8010 Sep 13, 2024
2eed579
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 17, 2024
2481082
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 19, 2024
5f21f65
Merge remote-tracking branch 'origin/main' into feature/combined-expe…
fabioh8010 Sep 20, 2024
ee62f9b
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 22, 2024
d58595c
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 24, 2024
575e4e6
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 25, 2024
8148fc1
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 26, 2024
c0c9d5e
Fix issue when submitting expense
fabioh8010 Sep 26, 2024
426599e
Fix ReportWelcomeText
fabioh8010 Sep 26, 2024
34b0536
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 27, 2024
539cf1d
Revert findSelfDMReportID change
fabioh8010 Sep 27, 2024
3c1f65f
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Sep 30, 2024
5e52ce1
Add logic to only display track button if user has a self DM
fabioh8010 Sep 30, 2024
17b07c8
Merge branch 'main' into feature/combined-expense-flow-48787
fabioh8010 Oct 1, 2024
fcd7690
Rename GLOBAL_CREATE to CREATE
fabioh8010 Oct 1, 2024
fdf3949
Use allBetas from ReportUtils
fabioh8010 Oct 1, 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
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2058,6 +2058,7 @@ const CONST = {
INVOICE: 'invoice',
SUBMIT: 'submit',
TRACK: 'track',
GLOBAL_CREATE: 'create',
fabioh8010 marked this conversation as resolved.
Show resolved Hide resolved
},
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)
fabioh8010 marked this conversation as resolved.
Show resolved Hide resolved
.filter(
(item): item is Exclude<IOUType, typeof CONST.IOU.TYPE.REQUEST | typeof CONST.IOU.TYPE.SEND | typeof CONST.IOU.TYPE.GLOBAL_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 @@ -1017,6 +1018,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 @@ -1011,6 +1012,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.GLOBAL_CREATE,
fabioh8010 marked this conversation as resolved.
Show resolved Hide resolved
];
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,
};
17 changes: 13 additions & 4 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6695,8 +6695,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.GLOBAL_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.GLOBAL_CREATE>
>;
}

/**
Expand Down Expand Up @@ -6935,12 +6937,19 @@ function getReportOfflinePendingActionAndErrors(report: OnyxEntry<Report>): Repo
/**
* Check if the report can create the expense with type is iouType
*/
function canCreateRequest(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>, iouType: ValueOf<typeof CONST.IOU.TYPE>): boolean {
function canCreateRequest(report: OnyxEntry<Report>, policy: OnyxEntry<Policy>, betas: OnyxEntry<Beta[]>, 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(betas ?? [])) {
requestOptions.push(CONST.IOU.TYPE.GLOBAL_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.GLOBAL_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.GLOBAL_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.GLOBAL_CREATE),
text: translate('iou.createExpense'),
onSelected: () =>
interceptAnonymousUser(() =>
IOU.startMoneyRequest(
CONST.IOU.TYPE.GLOBAL_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'),
Comment on lines -368 to -372
Copy link
Contributor

Choose a reason for hiding this comment

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

Where did you move this logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I moved to a variable called expenseMenuItems that is now responsible of returning the expense menu items

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.GLOBAL_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.GLOBAL_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
Loading