diff --git a/src/CONST.ts b/src/CONST.ts
index caaa26416e94..f4586573cfaa 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -297,6 +297,9 @@ const CONST = {
// Regex to get link in href prop inside of component
REGEX_LINK_IN_ANCHOR: /]*?\s+)?href="([^"]*)"/gi,
+ // Regex to read violation value from string given by backend
+ VIOLATION_LIMIT_REGEX: /[^0-9]+/g,
+
MERCHANT_NAME_MAX_LENGTH: 255,
MASKED_PAN_PREFIX: 'XXXXXXXXXXXX',
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index d083a46d7760..427e05052ae3 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -3,6 +3,7 @@ import type CONST from './CONST';
import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST';
import type * as FormTypes from './types/form';
import type * as OnyxTypes from './types/onyx';
+import type {Attendee} from './types/onyx/IOU';
import type Onboarding from './types/onyx/Onboarding';
import type AssertTypesEqual from './types/utils/AssertTypesEqual';
import type DeepValueOf from './types/utils/DeepValueOf';
@@ -112,6 +113,9 @@ const ONYXKEYS = {
/** Boolean flag only true when first set */
NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser',
+ /** This NVP contains list of at most 5 recent attendees */
+ NVP_RECENT_ATTENDEES: 'nvp_expensify_recentAttendees',
+
/** This NVP contains information about whether the onboarding flow was completed or not */
NVP_ONBOARDING: 'nvp_onboarding',
@@ -905,6 +909,7 @@ type OnyxValuesMapping = {
// The value of this nvp is a string representation of the date when the block expires, or an empty string if the user is not blocked
[ONYXKEYS.NVP_BLOCKED_FROM_CHAT]: string;
[ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string;
+ [ONYXKEYS.NVP_RECENT_ATTENDEES]: Attendee[];
[ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean;
[ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION]: boolean;
[ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 23ef269d8cfb..e6984d6c757b 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -447,6 +447,11 @@ const ROUTES = {
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '', reportActionID?: string) =>
getUrlWithBackToParam(`${action as string}/${iouType as string}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
+ MONEY_REQUEST_ATTENDEE: {
+ route: ':action/:iouType/attendees/:transactionID/:reportID',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') =>
+ getUrlWithBackToParam(`${action as string}/${iouType as string}/attendees/${transactionID}/${reportID}`, backTo),
+ },
SETTINGS_TAGS_ROOT: {
route: 'settings/:policyID/tags',
getRoute: (policyID: string, backTo = '') => getUrlWithBackToParam(`settings/${policyID}/tags`, backTo),
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 3a33975b2bbe..7f04b861dec5 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -219,6 +219,7 @@ const SCREENS = {
EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
RECEIPT: 'Money_Request_Receipt',
STATE_SELECTOR: 'Money_Request_State_Selector',
+ STEP_ATTENDEES: 'Money_Request_Attendee',
},
TRANSACTION_DUPLICATE: {
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index db56320e98ff..0eccd9834baf 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -34,7 +34,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
-import type {Participant} from '@src/types/onyx/IOU';
+import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {SplitShares} from '@src/types/onyx/Transaction';
import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
@@ -85,6 +85,9 @@ type MoneyRequestConfirmationListProps = MoneyRequestConfirmationListOnyxProps &
/** IOU amount */
iouAmount: number;
+ /** IOU attendees list */
+ iouAttendees?: Attendee[];
+
/** IOU comment */
iouComment?: string;
@@ -194,6 +197,7 @@ function MoneyRequestConfirmationList({
policyID = '',
reportID = '',
receiptPath = '',
+ iouAttendees,
iouComment,
receiptFilename = '',
iouCreated,
@@ -288,7 +292,12 @@ function MoneyRequestConfirmationList({
const formattedAmount = isDistanceRequestWithPendingRoute
? ''
: CurrencyUtils.convertToDisplayString(shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount, isDistanceRequest ? currency : iouCurrencyCode);
-
+ const formattedAmountPerAttendee = isDistanceRequestWithPendingRoute
+ ? ''
+ : CurrencyUtils.convertToDisplayString(
+ (shouldCalculateDistanceAmount ? distanceRequestAmount : iouAmount) / (iouAttendees?.length && iouAttendees.length > 0 ? iouAttendees.length : 1),
+ isDistanceRequest ? currency : iouCurrencyCode,
+ );
const isFocused = useIsFocused();
const [formError, debouncedFormError, setFormError] = useDebouncedState('');
@@ -899,8 +908,10 @@ function MoneyRequestConfirmationList({
didConfirm={!!didConfirm}
distance={distance}
formattedAmount={formattedAmount}
+ formattedAmountPerAttendee={formattedAmountPerAttendee}
formError={formError}
hasRoute={hasRoute}
+ iouAttendees={iouAttendees}
iouCategory={iouCategory}
iouComment={iouComment}
iouCreated={iouCreated}
@@ -1010,6 +1021,7 @@ export default withOnyx isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists), [isPolicyExpenseChat, policyTagLists]);
const isMultilevelTags = useMemo(() => PolicyUtils.isMultiLevelTags(policyTags), [policyTags]);
+ const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]);
const senderWorkspace = useMemo(() => {
const senderWorkspaceParticipant = selectedParticipants.find((participant) => participant.isSender);
@@ -514,6 +523,25 @@ function MoneyRequestConfirmationListFooter({
shouldShow: shouldShowTax,
isSupplementary: true,
},
+ {
+ item: (
+ item?.displayName ?? item?.login).join(', ')}
+ description={`${translate('iou.attendees')} ${
+ iouAttendees?.length && iouAttendees.length > 1 ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : ''
+ }`}
+ style={[styles.moneyRequestMenuItem]}
+ titleStyle={styles.flex1}
+ onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
+ interactive
+ shouldRenderAsHTML
+ />
+ ),
+ shouldShow: shouldShowAttendees,
+ isSupplementary: true,
+ },
{
item: (
TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]);
const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest);
const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport);
@@ -721,6 +724,25 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals
}}
/>
)}
+ {shouldShowAttendees && (
+
+ item?.displayName ?? item?.login).join(', ')}
+ description={`${translate('iou.attendees')} ${
+ transactionAttendees?.length && transactionAttendees.length > 1 ? `${formattedPerAttendeeAmount} ${translate('common.perPerson')}` : ''
+ }`}
+ style={[styles.moneyRequestMenuItem]}
+ titleStyle={styles.flex1}
+ onPress={() =>
+ Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1'))
+ }
+ interactive
+ shouldRenderAsHTML
+ />
+
+ )}
{shouldShowBillable && (
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 13a90d13d465..b2e175418813 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -17,6 +17,7 @@ import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
// eslint-disable-next-line no-restricted-imports
import type CursorStyles from '@styles/utils/cursor/types';
import type CONST from '@src/CONST';
+import type {Attendee} from '@src/types/onyx/IOU';
import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import type {SearchPersonalDetails, SearchReport, SearchReportAction, SearchTransaction} from '@src/types/onyx/SearchResults';
import type {ReceiptErrors} from '@src/types/onyx/Transaction';
@@ -234,6 +235,9 @@ type TransactionListItemType = ListItem &
/** Key used internally by React */
keyForList: string;
+
+ /** Attendees in the transaction */
+ attendees?: Attendee[];
};
type ReportActionListItemType = ListItem &
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 4050993a23f2..ad5ffdd336e9 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -259,6 +259,7 @@ const translations = {
firstName: 'First name',
lastName: 'Last name',
addCardTermsOfService: 'Expensify Terms of Service',
+ perPerson: 'per person',
phone: 'Phone',
phoneNumber: 'Phone number',
phoneNumberPlaceholder: '(xxx) xxx-xxxx',
@@ -974,6 +975,7 @@ const translations = {
atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses.',
splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.',
invalidMerchant: 'Please enter a correct merchant.',
+ atLeastOneAttendee: 'At least one attendee must be selected',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`,
enableWallet: 'Enable wallet',
@@ -1029,6 +1031,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.',
+ attendees: 'Attendees',
paymentComplete: 'Payment complete',
justTrackIt: 'Just track it (don’t submit it)',
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 1db6616571e2..ba04d2b2977a 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -243,6 +243,7 @@ const translations = {
not: 'No',
privacyPolicy: 'la Política de Privacidad de Expensify',
addCardTermsOfService: 'Términos de Servicio',
+ perPerson: 'por persona',
signIn: 'Conectarse',
signInWithGoogle: 'Iniciar sesión con Google',
signInWithApple: 'Iniciar sesión con Apple',
@@ -971,6 +972,7 @@ const translations = {
atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes.',
splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con miembros individuales. Por favor, actualiza tu selección.',
invalidMerchant: 'Por favor, introduce un comerciante correcto.',
+ atLeastOneAttendee: 'Debe seleccionarse al menos un asistente',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su billetera`,
enableWallet: 'Habilitar billetera',
@@ -1026,6 +1028,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.',
+ attendees: 'Asistentes',
paymentComplete: 'Pago completo',
justTrackIt: 'Solo guardarlo (no enviarlo)',
},
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index d0d37060065a..063be53a2eda 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -175,6 +175,7 @@ const WRITE_COMMANDS = {
DELETE_REPORT_FIELD: 'RemoveReportField',
SET_REPORT_NAME: 'RenameReport',
COMPLETE_SPLIT_BILL: 'CompleteSplitBill',
+ UPDATE_MONEY_REQUEST_ATTENDEES: 'UpdateMoneyRequestAttendees',
UPDATE_MONEY_REQUEST_DATE: 'UpdateMoneyRequestDate',
UPDATE_MONEY_REQUEST_BILLABLE: 'UpdateMoneyRequestBillable',
UPDATE_MONEY_REQUEST_MERCHANT: 'UpdateMoneyRequestMerchant',
@@ -592,6 +593,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_REPORT_NAME]: Parameters.SetReportNameParams;
[WRITE_COMMANDS.DELETE_REPORT_FIELD]: Parameters.DeleteReportFieldParams;
[WRITE_COMMANDS.COMPLETE_SPLIT_BILL]: Parameters.CompleteSplitBillParams;
+ [WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT]: Parameters.UpdateMoneyRequestParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_BILLABLE]: Parameters.UpdateMoneyRequestParams;
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index dcfdd4bbc73a..e0fd37db5b3b 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -3,7 +3,8 @@ import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {OnyxInputOrEntry, Report, Transaction} from '@src/types/onyx';
+import type {OnyxInputOrEntry, PersonalDetails, Report, Transaction} from '@src/types/onyx';
+import type {Attendee} from '@src/types/onyx/IOU';
import type {IOURequestType} from './actions/IOU';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
@@ -162,6 +163,24 @@ function shouldUseTransactionDraft(action: IOUAction | undefined) {
return action === CONST.IOU.ACTION.CREATE || isMovingTransactionFromTrackExpense(action);
}
+function formatCurrentUserToAttendee(currentUser?: PersonalDetails, reportID?: string) {
+ if (!currentUser) {
+ return;
+ }
+ const initialAttendee: Attendee = {
+ email: currentUser?.login,
+ login: currentUser?.login,
+ displayName: currentUser.displayName,
+ avatarUrl: currentUser.avatar?.toString(),
+ accountID: currentUser.accountID,
+ text: currentUser.login,
+ selected: true,
+ reportID,
+ };
+
+ return [initialAttendee];
+}
+
function shouldStartLocationPermissionFlow() {
return (
!lastLocationPermissionPrompt ||
@@ -179,5 +198,6 @@ export {
isValidMoneyRequestType,
navigateToStartMoneyRequestStep,
updateIOUOwnerAndTotal,
+ formatCurrentUserToAttendee,
shouldStartLocationPermissionFlow,
};
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 20c5f6d7dce9..f5109cbea74b 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -302,6 +302,19 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
);
}
+ const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'attendees' in reportActionOriginalMessage;
+ if (hasModifiedAttendees) {
+ buildMessageFragmentForValue(
+ reportActionOriginalMessage.oldAttendees ?? '',
+ reportActionOriginalMessage.attendees ?? '',
+ Localize.translateLocal('iou.attendees'),
+ false,
+ setFragments,
+ removalFragments,
+ changeFragments,
+ );
+ }
+
const message =
getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) +
getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) +
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index c9432fcbe649..2ee1f7d19be9 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -99,6 +99,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/AddDebitCardPage').default,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePaymentsPage').default,
[SCREENS.MONEY_REQUEST.STATE_SELECTOR]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default,
+ [SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: () => require('../../../../pages/iou/request/step/IOURequestStepAttendees').default,
});
const TravelModalStackNavigator = createModalStackNavigator({
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index f34060407b20..39da189af13c 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1186,6 +1186,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route,
[SCREENS.MONEY_REQUEST.STATE_SELECTOR]: {path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route, exact: true},
[SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.route,
+ [SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: ROUTES.MONEY_REQUEST_ATTENDEE.route,
[SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS,
[SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT,
[SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 757957f7bd1c..087df82c684f 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1162,6 +1162,13 @@ type MoneyRequestNavigatorParamList = {
backTo?: Routes;
currency?: string;
};
+ [SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: {
+ action: IOUAction;
+ iouType: Exclude;
+ transactionID: string;
+ reportID: string;
+ backTo: Routes;
+ };
};
type NewTaskNavigatorParamList = {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 06d2c8a6d56e..f414d2328ef6 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -35,7 +35,7 @@ import type {
Transaction,
TransactionViolation,
} from '@src/types/onyx';
-import type {Participant} from '@src/types/onyx/IOU';
+import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -178,6 +178,8 @@ type GetOptionsConfig = {
includeInvoiceRooms?: boolean;
includeDomainEmail?: boolean;
action?: IOUAction;
+ shouldAcceptName?: boolean;
+ recentAttendees?: Attendee[];
shouldBoldTitleByDefault?: boolean;
};
@@ -188,6 +190,7 @@ type GetUserToInviteConfig = {
selectedOptions?: Array>;
reportActions?: ReportActions;
showChatPreviewLine?: boolean;
+ shouldAcceptName?: boolean;
};
type MemberForList = {
@@ -219,7 +222,10 @@ type Options = {
type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean};
-type FilterOptionsConfig = Pick & {
+type FilterOptionsConfig = Pick<
+ GetOptionsConfig,
+ 'sortByReportTypeInSearch' | 'canInviteUser' | 'selectedOptions' | 'excludeUnknownUsers' | 'excludeLogins' | 'maxRecentReportsToShow' | 'shouldAcceptName'
+> & {
preferChatroomsOverThreads?: boolean;
preferPolicyExpenseChat?: boolean;
preferRecentExpenseReports?: boolean;
@@ -416,7 +422,7 @@ function getParticipantsOption(participant: ReportUtils.OptionData | Participant
const detail = getPersonalDetailsForAccountIDs([participant.accountID ?? -1], personalDetails)[participant.accountID ?? -1];
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const login = detail?.login || participant.login || '';
- const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login));
+ const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login) || participant.text);
return {
keyForList: String(detail?.accountID),
@@ -1624,6 +1630,7 @@ function canCreateOptimisticPersonalDetailOption({
* We create a new user option if the following conditions are satisfied:
* - There's no matching recent report and personal detail option
* - The searchValue is a valid email or phone number
+ * - If prop shouldAcceptName = true, the searchValue can be also a normal string
* - The searchValue isn't the current personal detail login
*/
function getUserToInviteOption({
@@ -1633,6 +1640,7 @@ function getUserToInviteOption({
selectedOptions = [],
reportActions = {},
showChatPreviewLine = false,
+ shouldAcceptName = false,
}: GetUserToInviteConfig): ReportUtils.OptionData | null {
const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchValue)));
const isCurrentUserLogin = isCurrentUser({login: searchValue} as PersonalDetails);
@@ -1642,7 +1650,7 @@ function getUserToInviteOption({
const isInOptionToExclude =
optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) !== -1;
- if (!searchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber) || isInOptionToExclude || excludeUnknownUsers) {
+ if (!searchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber && !shouldAcceptName) || isInOptionToExclude || excludeUnknownUsers) {
return null;
}
@@ -1659,7 +1667,7 @@ function getUserToInviteOption({
showChatPreviewLine,
});
userToInvite.isOptimisticAccount = true;
- userToInvite.login = searchValue;
+ userToInvite.login = isValidEmail || isValidPhoneNumber ? searchValue : '';
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
userToInvite.text = userToInvite.text || searchValue;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
@@ -1722,6 +1730,7 @@ function getOptions(
includeInvoiceRooms = false,
includeDomainEmail = false,
action,
+ recentAttendees,
shouldBoldTitleByDefault = true,
}: GetOptionsConfig,
): Options {
@@ -1897,7 +1906,7 @@ function getOptions(
optionsToExclude.push({login});
});
- let recentReportOptions = [];
+ let recentReportOptions: ReportUtils.OptionData[] = [];
let personalDetailsOptions: ReportUtils.OptionData[] = [];
const preferRecentExpenseReports = action === CONST.IOU.ACTION.CREATE;
@@ -1978,6 +1987,9 @@ function getOptions(
}
}
}
+ } else if (recentAttendees && recentAttendees?.length > 0) {
+ recentAttendees.filter((attendee) => attendee.login ?? attendee.displayName).forEach((a) => optionsToExclude.push({login: a.login ?? a.displayName}));
+ recentReportOptions = recentAttendees as ReportUtils.OptionData[];
}
const personalDetailsOptionsToExclude = [...optionsToExclude, {login: currentUserLogin}];
@@ -2210,6 +2222,52 @@ function getFilteredOptions(params: FilteredOptionsParamsWithDefaultSearchValue
);
}
+function getAttendeeOptions(
+ reports: Array>,
+ personalDetails: Array>,
+ betas: OnyxEntry,
+ attendees: Attendee[],
+ recentAttendees: Attendee[],
+ includeOwnedWorkspaceChats = false,
+ includeP2P = true,
+ canInviteUser = true,
+ includeInvoiceRooms = false,
+ action: IOUAction | undefined = undefined,
+ sortByReportTypeInSearch = false,
+) {
+ return getOptions(
+ {reports, personalDetails},
+ {
+ betas,
+ searchInputValue: '',
+ selectedOptions: attendees,
+ excludeLogins: CONST.EXPENSIFY_EMAILS,
+ includeOwnedWorkspaceChats,
+ includeRecentReports: false,
+ includeP2P,
+ includeCategories: false,
+ categories: {},
+ recentlyUsedCategories: [],
+ includeTags: false,
+ tags: {},
+ recentlyUsedTags: [],
+ canInviteUser,
+ includeSelectedOptions: false,
+ includeTaxRates: false,
+ maxRecentReportsToShow: 0,
+ taxRates: {} as TaxRatesWithDefault,
+ includeSelfDM: false,
+ includePolicyReportFieldOptions: false,
+ policyReportFieldOptions: [],
+ recentlyUsedPolicyReportFieldOptions: [],
+ includeInvoiceRooms,
+ action,
+ sortByReportTypeInSearch,
+ recentAttendees,
+ },
+ );
+}
+
/**
* Build the options for the Share Destination for a Task
*/
@@ -2614,6 +2672,7 @@ export {
getCurrentUserSearchTerms,
getEmptyOptions,
shouldUseBoldText,
+ getAttendeeOptions,
getAlternateText,
pickUserToInvite,
hasReportErrors,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 2588c1c89039..1094e379d6fc 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -44,7 +44,7 @@ import type {
TransactionViolation,
UserWallet,
} from '@src/types/onyx';
-import type {Participant} from '@src/types/onyx/IOU';
+import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {SelectedParticipant} from '@src/types/onyx/NewGroupChatDraft';
import type {OriginalMessageExportedToIntegration} from '@src/types/onyx/OldDotAction';
import type Onboarding from '@src/types/onyx/Onboarding';
@@ -417,6 +417,7 @@ type OptimisticTaskReport = Pick<
type TransactionDetails = {
created: string;
amount: number;
+ attendees: Attendee[];
taxAmount?: number;
taxCode?: string;
currency: string;
@@ -3007,6 +3008,7 @@ function getTransactionDetails(transaction: OnyxInputOrEntry, creat
return {
created: TransactionUtils.getFormattedCreated(transaction, createdDateFormat),
amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
+ attendees: TransactionUtils.getAttendees(transaction),
taxAmount: TransactionUtils.getTaxAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)),
taxCode: TransactionUtils.getTaxCode(transaction),
currency: TransactionUtils.getCurrency(transaction),
@@ -3585,6 +3587,9 @@ function getModifiedExpenseOriginalMessage(
originalMessage.oldMerchant = TransactionUtils.getMerchant(oldTransaction);
originalMessage.merchant = transactionChanges?.merchant;
}
+ if ('attendees' in transactionChanges) {
+ [originalMessage.oldAttendees, originalMessage.attendees] = TransactionUtils.getFormattedAttendees(transactionChanges?.attendees, TransactionUtils.getAttendees(oldTransaction));
+ }
// The amount is always a combination of the currency and the number value so when one changes we need to store both
// to match how we handle the modified expense action in oldDot
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index e2c2ee6f8096..bf5073aba27c 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -20,8 +20,10 @@ import * as ReportConnection from '@libs/ReportConnection';
import * as ReportUtils from '@libs/ReportUtils';
import type {IOURequestType} from '@userActions/IOU';
import CONST from '@src/CONST';
+import type {IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
+import type {Attendee} from '@src/types/onyx/IOU';
import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import type DeepValueOf from '@src/types/utils/DeepValueOf';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -126,6 +128,7 @@ function buildOptimisticTransaction(
currency: string,
reportID: string,
comment = '',
+ attendees: Attendee[] = [],
created = '',
source = '',
originalTransactionID = '',
@@ -179,6 +182,7 @@ function buildOptimisticTransaction(
taxAmount,
billable,
reimbursable,
+ attendees,
};
}
@@ -207,6 +211,13 @@ function isMerchantMissing(transaction: OnyxEntry) {
return isMerchantEmpty;
}
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function shouldShowAttendees(iouType: IOUType, policy: OnyxEntry): boolean {
+ return false;
+ // To be renabled once feature is complete: https://github.com/Expensify/App/issues/44725
+ // return iouType === CONST.IOU.TYPE.SUBMIT && !!policy?.id && (policy?.type === CONST.POLICY.TYPE.CORPORATE || policy?.type === CONST.POLICY.TYPE.TEAM);
+}
+
/**
* Check if the merchant is partial i.e. `(none)`
*/
@@ -312,6 +323,10 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
updatedTransaction.tag = transactionChanges.tag;
}
+ if (Object.hasOwn(transactionChanges, 'attendees')) {
+ updatedTransaction.modifiedAttendees = transactionChanges?.attendees;
+ }
+
if (
shouldUpdateReceiptState &&
shouldStopSmartscan &&
@@ -335,6 +350,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'taxAmount') && {taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
...(Object.hasOwn(transactionChanges, 'taxCode') && {taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
+ ...(Object.hasOwn(transactionChanges, 'attendees') && {attendees: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
};
return updatedTransaction;
@@ -440,6 +456,22 @@ function getMerchantOrDescription(transaction: OnyxEntry) {
return !isMerchantMissing(transaction) ? getMerchant(transaction) : getDescription(transaction);
}
+/**
+ * Return the list of modified attendees if present otherwise list of attendees
+ */
+function getAttendees(transaction: OnyxInputOrEntry): Attendee[] {
+ return transaction?.modifiedAttendees ? transaction.modifiedAttendees : transaction?.attendees ?? [];
+}
+
+/**
+ * Return the list of attendees as a string and modified list of attendees as a string if present.
+ */
+function getFormattedAttendees(modifiedAttendees?: Attendee[], attendees?: Attendee[]): [string, string] {
+ const oldAttendees = modifiedAttendees ?? [];
+ const newAttendees = attendees ?? [];
+ return [oldAttendees.map((item) => item.displayName ?? item.login).join(', '), newAttendees.map((item) => item.displayName ?? item.login).join(', ')];
+}
+
/**
* Return the reimbursable value. Defaults to true to match BE logic.
*/
@@ -1140,6 +1172,7 @@ export {
isManualRequest,
isScanRequest,
getAmount,
+ getAttendees,
getTaxAmount,
getTaxCode,
getCurrency,
@@ -1147,6 +1180,7 @@ export {
getCardID,
getOriginalCurrency,
getOriginalAmount,
+ getFormattedAttendees,
getMerchant,
getMerchantOrDescription,
getMCCGroup,
@@ -1206,6 +1240,7 @@ export {
removeSettledAndApprovedTransactions,
getCardName,
hasReceiptSource,
+ shouldShowAttendees,
};
export type {TransactionChanges};
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index eff3d2b704a7..b49a7be66b31 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -343,6 +343,11 @@ const ViolationsUtils = {
return violation.name as never;
}
},
+
+ // We have to use regex, because Violation limit is given in a inconvenient form: "$2,000.00"
+ getViolationAmountLimit(violation: TransactionViolation): number {
+ return Number(violation.data?.formattedLimit?.replace(CONST.VIOLATION_LIMIT_REGEX, ''));
+ },
};
export default ViolationsUtils;
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index f76bb5bf1a4f..7ce9b9dfb272 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -61,7 +61,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
-import type {Participant, Split} from '@src/types/onyx/IOU';
+import type {Attendee, Participant, Split} from '@src/types/onyx/IOU';
import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type ReportAction from '@src/types/onyx/ReportAction';
@@ -359,6 +359,7 @@ function initMoneyRequest(
// Use set() here so that there is no way that data will be leaked between objects when it gets reset
Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${newTransactionID}`, {
amount: 0,
+ attendees: IOUUtils.formatCurrentUserToAttendee(currentUserPersonalDetails, reportID),
comment,
created,
currency,
@@ -426,6 +427,10 @@ function setMoneyRequestMerchant(transactionID: string, merchant: string, isDraf
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {merchant});
}
+function setMoneyRequestAttendees(transactionID: string, attendees: Attendee[], isDraft: boolean) {
+ Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {attendees});
+}
+
function setMoneyRequestPendingFields(transactionID: string, pendingFields: OnyxTypes.Transaction['pendingFields']) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields});
}
@@ -1910,6 +1915,7 @@ function getSendInvoiceInformation(
currency,
optimisticInvoiceReport.reportID,
trimmedComment,
+ [],
created,
'',
'',
@@ -2031,6 +2037,7 @@ function getMoneyRequestInformation(
payeeEmail = currentUserEmail,
moneyRequestReportID = '',
linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction,
+ attendees?: Attendee[],
existingTransaction: OnyxEntry | undefined = undefined,
): MoneyRequestInformation {
const payerEmail = PhoneNumber.addSMSDomainIfPhoneNumber(participant.login ?? '');
@@ -2090,6 +2097,7 @@ function getMoneyRequestInformation(
currency,
iouReport.reportID,
comment,
+ attendees,
created,
'',
'',
@@ -2330,6 +2338,7 @@ function getTrackExpenseInformation(
currency,
shouldUseMoneyReport && iouReport ? iouReport.reportID : '-1',
comment,
+ [],
created,
'',
'',
@@ -2489,6 +2498,7 @@ function getUpdateMoneyRequestParams(
policyTagList: OnyxTypes.OnyxInputOrEntry,
policyCategories: OnyxTypes.OnyxInputOrEntry,
onlyIncludeChangedFields: boolean,
+ violations?: OnyxEntry,
): UpdateMoneyRequestData {
const optimisticData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];
@@ -2732,6 +2742,19 @@ function getUpdateMoneyRequestParams(
}
}
+ const overLimitViolation = violations?.find((violation) => violation.name === 'overLimit');
+ // Update violation limit, if we modify attendees. The given limit value is for a single attendee, if we have multiple attendees we should multpiply limit by attende count
+ if ('attendees' in transactionChanges && !!overLimitViolation) {
+ const limitForSingleAttendee = ViolationsUtils.getViolationAmountLimit(overLimitViolation);
+ if (limitForSingleAttendee * (transactionChanges?.attendees?.length ?? 1) > Math.abs(TransactionUtils.getAmount(transaction))) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
+ value: violations?.filter((violation) => violation.name !== 'overLimit') ?? [],
+ });
+ }
+ }
+
const clearedPendingFields = Object.fromEntries(Object.keys(updatedTransaction?.pendingFields ?? transactionChanges).map((key) => [key, null]));
// Clear out the error fields and loading states on success
@@ -3051,6 +3074,24 @@ function updateMoneyRequestMerchant(
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT, params, onyxData);
}
+/** Updates the attendees list of an expense */
+function updateMoneyRequestAttendees(
+ transactionID: string,
+ transactionThreadReportID: string,
+ attendees: Attendee[],
+ policy: OnyxEntry,
+ policyTagList: OnyxEntry,
+ policyCategories: OnyxEntry,
+ violations: OnyxEntry,
+) {
+ const transactionChanges: TransactionChanges = {
+ attendees,
+ };
+ const data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true, violations);
+ const {params, onyxData} = data;
+ API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_ATTENDEES, params, onyxData);
+}
+
/** Updates the tag of an expense */
function updateMoneyRequestTag(
transactionID: string,
@@ -3308,6 +3349,7 @@ function convertTrackedExpenseToRequest(
comment: string,
merchant: string,
created: string,
+ attendees?: Attendee[],
receipt?: Receipt,
) {
const {optimisticData, successData, failureData} = onyxData;
@@ -3332,6 +3374,7 @@ function convertTrackedExpenseToRequest(
failureData?.push(...moveTransactionFailureData);
const parameters = {
+ attendees,
amount,
currency,
comment,
@@ -3510,6 +3553,7 @@ function shareTrackedExpense(
function requestMoney(
report: OnyxEntry,
amount: number,
+ attendees: Attendee[] | undefined,
currency: string,
created: string,
merchant: string,
@@ -3576,6 +3620,7 @@ function requestMoney(
payeeEmail,
moneyRequestReportID,
linkedTrackedExpenseReportAction,
+ attendees,
);
const activeReportID = isMoneyRequestReport ? report?.reportID : chatReport.reportID;
@@ -3605,6 +3650,7 @@ function requestMoney(
comment,
merchant,
created,
+ attendees,
receipt,
);
break;
@@ -3990,6 +4036,7 @@ function createSplitsAndOnyxData(
currency,
CONST.REPORT.SPLIT_REPORTID,
comment,
+ [],
created,
'',
'',
@@ -4234,6 +4281,7 @@ function createSplitsAndOnyxData(
currency,
oneOnOneIOUReport.reportID,
comment,
+ [],
created,
CONST.IOU.TYPE.SPLIT,
splitTransaction.transactionID,
@@ -4581,6 +4629,7 @@ function startSplitBill({
currency,
CONST.REPORT.SPLIT_REPORTID,
comment,
+ [],
'',
'',
'',
@@ -4992,6 +5041,7 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA
currency ?? '',
oneOnOneIOUReport?.reportID ?? '-1',
updatedTransaction?.comment?.comment,
+ [],
updatedTransaction?.modifiedCreated,
CONST.IOU.TYPE.SPLIT,
transactionID,
@@ -5231,6 +5281,7 @@ function createDistanceRequest(
currentUserEmail,
moneyRequestReportID,
undefined,
+ undefined,
existingTransaction,
);
onyxData = moneyRequestOnyxData;
@@ -8523,6 +8574,7 @@ export {
setDraftSplitTransaction,
setIndividualShare,
setMoneyRequestAmount,
+ setMoneyRequestAttendees,
setMoneyRequestBillable,
setMoneyRequestCategory,
setMoneyRequestCreated,
@@ -8547,6 +8599,7 @@ export {
trackExpense,
unapproveExpenseReport,
unholdRequest,
+ updateMoneyRequestAttendees,
updateMoneyRequestAmountAndCurrency,
updateMoneyRequestBillable,
updateMoneyRequestCategory,
diff --git a/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx
new file mode 100644
index 000000000000..8732efd2e72d
--- /dev/null
+++ b/src/pages/iou/request/MoneyRequestAttendeeSelector.tsx
@@ -0,0 +1,311 @@
+import lodashIsEqual from 'lodash/isEqual';
+import lodashReject from 'lodash/reject';
+import React, {memo, useCallback, useEffect, useMemo} from 'react';
+import type {GestureResponderEvent} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import Button from '@components/Button';
+import EmptySelectionListContent from '@components/EmptySelectionListContent';
+import FormHelpMessage from '@components/FormHelpMessage';
+import {usePersonalDetails, useSession} from '@components/OnyxProvider';
+import {useOptionsList} from '@components/OptionListContextProvider';
+import SelectionList from '@components/SelectionList';
+import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem';
+import useDebouncedState from '@hooks/useDebouncedState';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import usePolicy from '@hooks/usePolicy';
+import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus';
+import useThemeStyles from '@hooks/useThemeStyles';
+import * as DeviceCapabilities from '@libs/DeviceCapabilities';
+import * as OptionsListUtils from '@libs/OptionsListUtils';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as Report from '@userActions/Report';
+import type {IOUAction, IOUType} from '@src/CONST';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Attendee} from '@src/types/onyx/IOU';
+
+type MoneyRequestAttendeesSelectorProps = {
+ /** Callback to request parent modal to go to next step, which should be split */
+ onFinish: (value?: string) => void;
+
+ /** Callback to add participants in MoneyRequestModal */
+ onAttendeesAdded: (value: Attendee[]) => void;
+
+ /** Selected participants from MoneyRequestModal with login */
+ attendees?: Attendee[];
+
+ /** The type of IOU report, i.e. split, request, send, track */
+ iouType: IOUType;
+
+ /** The action of the IOU, i.e. create, split, move */
+ action: IOUAction;
+};
+
+function MoneyRequestAttendeeSelector({attendees = [], onFinish, onAttendeesAdded, iouType, action}: MoneyRequestAttendeesSelectorProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState('');
+ const {isOffline} = useNetwork();
+ const personalDetails = usePersonalDetails();
+ const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus();
+ const [betas] = useOnyx(ONYXKEYS.BETAS);
+ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
+ const session = useSession();
+ const isCurrentUserAttendee = attendees.some((attendee) => attendee.accountID === session.accountID);
+ const [recentAttendees] = useOnyx(ONYXKEYS.NVP_RECENT_ATTENDEES);
+ const policy = usePolicy(activePolicyID);
+ const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false});
+ const {options, areOptionsInitialized} = useOptionsList({
+ shouldInitialize: didScreenTransitionEnd,
+ });
+ const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]);
+ const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';
+
+ const isPaidGroupPolicy = useMemo(() => PolicyUtils.isPaidGroupPolicy(policy), [policy]);
+ const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].some((option) => option === action);
+
+ useEffect(() => {
+ Report.searchInServer(debouncedSearchTerm.trim());
+ }, [debouncedSearchTerm]);
+
+ const defaultOptions = useMemo(() => {
+ if (!areOptionsInitialized || !didScreenTransitionEnd) {
+ OptionsListUtils.getEmptyOptions();
+ }
+ const optionList = OptionsListUtils.getAttendeeOptions(
+ options.reports,
+ options.personalDetails,
+ betas,
+ attendees,
+ recentAttendees ?? [],
+ iouType === CONST.IOU.TYPE.SUBMIT && action !== CONST.IOU.ACTION.SUBMIT,
+ !isCategorizeOrShareAction,
+ !isCategorizeOrShareAction,
+ iouType === CONST.IOU.TYPE.INVOICE,
+ action,
+ isPaidGroupPolicy,
+ );
+ if (optionList.currentUserOption && !isCurrentUserAttendee) {
+ optionList.recentReports = [optionList.currentUserOption, ...optionList.personalDetails];
+ }
+ return optionList;
+ }, [
+ areOptionsInitialized,
+ didScreenTransitionEnd,
+ options.reports,
+ options.personalDetails,
+ betas,
+ attendees,
+ recentAttendees,
+ iouType,
+ action,
+ isCategorizeOrShareAction,
+ isPaidGroupPolicy,
+ isCurrentUserAttendee,
+ ]);
+
+ const chatOptions = useMemo(() => {
+ if (!areOptionsInitialized) {
+ return {
+ userToInvite: null,
+ recentReports: [],
+ personalDetails: [],
+ currentUserOption: null,
+ headerMessage: '',
+ categoryOptions: [],
+ tagOptions: [],
+ taxRatesOptions: [],
+ };
+ }
+ const newOptions = OptionsListUtils.filterOptions(defaultOptions, debouncedSearchTerm, {
+ excludeLogins: CONST.EXPENSIFY_EMAILS,
+ maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
+ preferPolicyExpenseChat: isPaidGroupPolicy,
+ shouldAcceptName: true,
+ });
+ return newOptions;
+ }, [areOptionsInitialized, defaultOptions, debouncedSearchTerm, isPaidGroupPolicy]);
+
+ /**
+ * Returns the sections needed for the OptionsSelector
+ */
+ const [sections, header] = useMemo(() => {
+ const newSections: OptionsListUtils.CategorySection[] = [];
+ if (!areOptionsInitialized || !didScreenTransitionEnd) {
+ return [newSections, ''];
+ }
+ const fiveRecents = [...chatOptions.recentReports].slice(0, 5);
+ const restOfRecents = [...chatOptions.recentReports].slice(5);
+ const contactsWithRestOfRecents = [...restOfRecents, ...chatOptions.personalDetails];
+
+ const formatResults = OptionsListUtils.formatSectionsFromSearchTerm(
+ debouncedSearchTerm,
+ attendees.map((attendee) => ({...attendee, reportID: attendee.reportID ?? '-1'})),
+ chatOptions.recentReports,
+ chatOptions.personalDetails,
+ personalDetails,
+ true,
+ );
+ newSections.push(formatResults.section);
+
+ newSections.push({
+ title: translate('common.recents'),
+ data: fiveRecents,
+ shouldShow: fiveRecents.length > 0,
+ });
+
+ newSections.push({
+ title: translate('common.contacts'),
+ data: contactsWithRestOfRecents,
+ shouldShow: contactsWithRestOfRecents.length > 0,
+ });
+
+ if (
+ chatOptions.userToInvite &&
+ !OptionsListUtils.isCurrentUser({...chatOptions.userToInvite, accountID: chatOptions.userToInvite?.accountID ?? -1, status: chatOptions.userToInvite?.status ?? undefined})
+ ) {
+ newSections.push({
+ title: undefined,
+ data: [chatOptions.userToInvite].map((participant) => {
+ const isPolicyExpenseChat = participant?.isPolicyExpenseChat ?? false;
+ return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails);
+ }),
+ shouldShow: true,
+ });
+ }
+
+ const headerMessage = OptionsListUtils.getHeaderMessage(
+ (chatOptions.personalDetails ?? []).length + (chatOptions.recentReports ?? []).length !== 0,
+ !!chatOptions?.userToInvite,
+ debouncedSearchTerm.trim(),
+ attendees.some((attendee) => OptionsListUtils.getPersonalDetailSearchTerms(attendee).join(' ').toLowerCase().includes(cleanSearchTerm)),
+ );
+
+ return [newSections, headerMessage];
+ }, [
+ areOptionsInitialized,
+ didScreenTransitionEnd,
+ debouncedSearchTerm,
+ attendees,
+ chatOptions.recentReports,
+ chatOptions.personalDetails,
+ chatOptions.userToInvite,
+ personalDetails,
+ translate,
+ cleanSearchTerm,
+ ]);
+
+ const addAttendeeToSelection = useCallback(
+ (option: Attendee) => {
+ const isOptionSelected = (selectedOption: Attendee) => {
+ if (selectedOption.accountID && selectedOption.accountID === option?.accountID) {
+ return true;
+ }
+
+ return false;
+ };
+ const isOptionInList = attendees.some(isOptionSelected);
+ let newSelectedOptions: Attendee[];
+
+ if (isOptionInList) {
+ newSelectedOptions = lodashReject(attendees, isOptionSelected);
+ } else {
+ newSelectedOptions = [
+ ...attendees,
+ {
+ accountID: option.accountID ?? -1,
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ login: option.login || option.text,
+ displayName: option.text,
+ selected: true,
+ searchText: option.searchText,
+ iouType,
+ },
+ ];
+ }
+
+ onAttendeesAdded(newSelectedOptions);
+ },
+ [attendees, iouType, onAttendeesAdded],
+ );
+
+ const shouldShowErrorMessage = attendees.length < 1;
+
+ const handleConfirmSelection = useCallback(
+ (_keyEvent?: GestureResponderEvent | KeyboardEvent, option?: Attendee) => {
+ if (shouldShowErrorMessage || (!attendees.length && !option)) {
+ return;
+ }
+
+ onFinish(CONST.IOU.TYPE.SUBMIT);
+ },
+ [shouldShowErrorMessage, onFinish, attendees],
+ );
+
+ const showLoadingPlaceholder = useMemo(() => !areOptionsInitialized || !didScreenTransitionEnd, [areOptionsInitialized, didScreenTransitionEnd]);
+
+ const optionLength = useMemo(() => {
+ if (!areOptionsInitialized) {
+ return 0;
+ }
+ return sections.reduce((acc, section) => acc + section.data.length, 0);
+ }, [areOptionsInitialized, sections]);
+
+ const shouldShowListEmptyContent = useMemo(() => optionLength === 0 && !showLoadingPlaceholder, [optionLength, showLoadingPlaceholder]);
+
+ const footerContent = useMemo(() => {
+ if (!shouldShowErrorMessage && !attendees.length) {
+ return;
+ }
+
+ return (
+ <>
+ {shouldShowErrorMessage && (
+
+ )}
+
+ {!isCategorizeOrShareAction && (
+
+ )}
+ >
+ );
+ }, [handleConfirmSelection, attendees.length, shouldShowErrorMessage, styles, translate, isCategorizeOrShareAction]);
+
+ return (
+ }
+ headerMessage={header}
+ showLoadingPlaceholder={showLoadingPlaceholder}
+ canSelectMultiple
+ isLoadingNewOptions={!!isSearchingForReports}
+ shouldShowListEmptyContent={shouldShowListEmptyContent}
+ />
+ );
+}
+
+MoneyRequestAttendeeSelector.displayName = 'MoneyRequestAttendeeSelector';
+
+export default memo(MoneyRequestAttendeeSelector, (prevProps, nextProps) => lodashIsEqual(prevProps.attendees, nextProps.attendees) && prevProps.iouType === nextProps.iouType);
diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx
index 4e2cc03b8cc4..ce3d03138dde 100644
--- a/src/pages/iou/request/step/IOURequestStepAmount.tsx
+++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx
@@ -226,6 +226,7 @@ function IOURequestStepAmount({
IOU.requestMoney(
report,
backendAmount,
+ transaction?.attendees,
currency,
transaction?.created ?? '',
CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT,
diff --git a/src/pages/iou/request/step/IOURequestStepAttendees.tsx b/src/pages/iou/request/step/IOURequestStepAttendees.tsx
new file mode 100644
index 000000000000..f1d253db0c18
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestStepAttendees.tsx
@@ -0,0 +1,86 @@
+import lodashIsEqual from 'lodash/isEqual';
+import React, {useCallback, useState} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import useLocalize from '@hooks/useLocalize';
+import usePrevious from '@hooks/usePrevious';
+import Navigation from '@libs/Navigation/Navigation';
+import * as TransactionUtils from '@libs/TransactionUtils';
+import MoneyRequestAttendeeSelector from '@pages/iou/request/MoneyRequestAttendeeSelector';
+import * as IOU from '@userActions/IOU';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {Attendee} from '@src/types/onyx/IOU';
+import StepScreenWrapper from './StepScreenWrapper';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
+import withWritableReportOrNotFound from './withWritableReportOrNotFound';
+
+type IOURequestStepAttendeesOnyxProps = {
+ /** The policy of the report */
+ policy: OnyxEntry;
+
+ /** Collection of categories attached to a policy */
+ policyCategories: OnyxEntry;
+
+ /** Collection of tags attached to a policy */
+ policyTags: OnyxEntry;
+};
+
+type IOURequestStepAttendeesProps = IOURequestStepAttendeesOnyxProps & WithWritableReportOrNotFoundProps;
+
+function IOURequestStepAttendees({
+ route: {
+ params: {transactionID, reportID, iouType, backTo, action},
+ },
+ policy,
+ policyTags,
+ policyCategories,
+}: IOURequestStepAttendeesProps) {
+ const isEditing = action === CONST.IOU.ACTION.EDIT;
+ const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || -1}`);
+ const [attendees, setAttendees] = useState(TransactionUtils.getAttendees(transaction));
+ const previousAttendees = usePrevious(attendees);
+ const {translate} = useLocalize();
+ const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
+
+ const saveAttendees = useCallback(() => {
+ if (attendees.length <= 0) {
+ return;
+ }
+ if (!lodashIsEqual(previousAttendees, attendees)) {
+ IOU.setMoneyRequestAttendees(transactionID, attendees, !isEditing);
+ if (isEditing) {
+ IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations);
+ }
+ }
+
+ Navigation.goBack(backTo);
+ }, [attendees, backTo, isEditing, policy, policyCategories, policyTags, previousAttendees, reportID, transactionID, violations]);
+
+ const navigateBack = () => {
+ Navigation.goBack(backTo);
+ };
+
+ return (
+
+ setAttendees(v)}
+ attendees={attendees}
+ iouType={iouType}
+ action={action}
+ />
+
+ );
+}
+
+IOURequestStepAttendees.displayName = 'IOURequestStepAttendees';
+
+export default withWritableReportOrNotFound(IOURequestStepAttendees);
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
index 835e37ef877e..942b472734a2 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
@@ -241,6 +241,7 @@ function IOURequestStepConfirmation({
IOU.requestMoney(
report,
transaction.amount,
+ transaction.attendees,
transaction.currency,
transaction.created,
transaction.merchant,
@@ -638,6 +639,7 @@ function IOURequestStepConfirmation({
transaction={transaction}
selectedParticipants={participants}
iouAmount={Math.abs(transaction?.amount ?? 0)}
+ iouAttendees={transaction?.attendees ?? []}
iouComment={transaction?.comment?.comment ?? ''}
iouCurrencyCode={transaction?.currency}
iouIsBillable={transaction?.billable}
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index ce7b48f53199..db7aac8268e6 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -252,6 +252,7 @@ function IOURequestStepScan({
IOU.requestMoney(
report,
0,
+ transaction?.attendees,
transaction?.currency ?? 'USD',
transaction?.created ?? '',
'',
@@ -263,7 +264,7 @@ function IOURequestStepScan({
);
}
},
- [currentUserPersonalDetails.accountID, currentUserPersonalDetails.login, iouType, report, transaction?.created, transaction?.currency],
+ [currentUserPersonalDetails.accountID, currentUserPersonalDetails.login, iouType, report, transaction?.attendees, transaction?.created, transaction?.currency],
);
const navigateToConfirmationStep = useCallback(
(file: FileObject, source: string, locationPermissionGranted = false) => {
@@ -345,6 +346,7 @@ function IOURequestStepScan({
IOU.requestMoney(
report,
0,
+ transaction?.attendees,
transaction?.currency ?? 'USD',
transaction?.created ?? '',
'',
@@ -388,6 +390,7 @@ function IOURequestStepScan({
[
backTo,
transaction?.isFromGlobalCreate,
+ transaction?.attendees,
transaction?.currency,
transaction?.created,
iouType,
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
index 0b05d44ddba8..1f71f174dd23 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
@@ -285,6 +285,7 @@ function IOURequestStepScan({
IOU.requestMoney(
report,
0,
+ transaction?.attendees,
transaction?.currency ?? 'USD',
transaction?.created ?? '',
'',
@@ -296,7 +297,7 @@ function IOURequestStepScan({
);
}
},
- [currentUserPersonalDetails.accountID, currentUserPersonalDetails.login, iouType, report, transaction?.created, transaction?.currency],
+ [currentUserPersonalDetails.accountID, currentUserPersonalDetails.login, iouType, report, transaction?.attendees, transaction?.created, transaction?.currency],
);
const navigateToConfirmationStep = useCallback(
@@ -379,6 +380,7 @@ function IOURequestStepScan({
IOU.requestMoney(
report,
0,
+ transaction?.attendees,
transaction?.currency ?? 'USD',
transaction?.created ?? '',
'',
@@ -422,6 +424,7 @@ function IOURequestStepScan({
[
backTo,
transaction?.isFromGlobalCreate,
+ transaction?.attendees,
transaction?.currency,
transaction?.created,
iouType,
diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
index 2361d58dc2be..c4fd8bb2eec7 100644
--- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
+++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
@@ -40,7 +40,8 @@ type MoneyRequestRouteName =
| typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT
| typeof SCREENS.MONEY_REQUEST.STEP_SCAN
| typeof SCREENS.MONEY_REQUEST.STEP_SEND_FROM
- | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO;
+ | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO
+ | typeof SCREENS.MONEY_REQUEST.STEP_ATTENDEES;
type Route = RouteProp;
diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx
index 21dee96609b4..2ee05f486ed6 100644
--- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx
+++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx
@@ -44,6 +44,8 @@ function QuickbooksAdvancedPage({policy}: WithPolicyConnectionsProps) {
() => invoiceAccountCollectionOptions?.find(({id}) => id === collectionAccountID)?.name,
[invoiceAccountCollectionOptions, collectionAccountID],
);
+ const autoCreateVendorConst = CONST.QUICKBOOKS_CONFIG.AUTO_CREATE_VENDOR;
+ const defaultVendorConst = CONST.QUICKBOOKS_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR;
const sectionMenuItems = [
{
@@ -116,12 +118,12 @@ function QuickbooksAdvancedPage({policy}: WithPolicyConnectionsProps) {
QuickbooksOnline.updateQuickbooksOnlineAutoCreateVendor(
policyID,
{
- [CONST.QUICKBOOKS_CONFIG.AUTO_CREATE_VENDOR]: isOn,
- [CONST.QUICKBOOKS_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR]: nonReimbursableVendorUpdateValue,
+ [autoCreateVendorConst]: isOn,
+ [defaultVendorConst]: nonReimbursableVendorUpdateValue,
},
{
- [CONST.QUICKBOOKS_CONFIG.AUTO_CREATE_VENDOR]: !!qboConfig?.autoCreateVendor,
- [CONST.QUICKBOOKS_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR]: nonReimbursableVendorCurrentValue,
+ [autoCreateVendorConst]: !!qboConfig?.autoCreateVendor,
+ [defaultVendorConst]: nonReimbursableVendorCurrentValue,
},
);
},
diff --git a/src/stories/ReportActionItemImages.stories.tsx b/src/stories/ReportActionItemImages.stories.tsx
index 19632d2c0070..ce7e4719a97f 100644
--- a/src/stories/ReportActionItemImages.stories.tsx
+++ b/src/stories/ReportActionItemImages.stories.tsx
@@ -50,6 +50,7 @@ DisplayEReceipt.args = {
thumbnail: '',
transaction: {
transactionID: 'FAKE_3',
+ attendees: [{email: 'test@expensify.com'}],
amount: 1000,
currency: 'USD',
cardID: 5,
@@ -74,6 +75,7 @@ DisplayMultipleEReceipts.args = {
thumbnail: '',
transaction: {
transactionID: 'FAKE_3',
+ attendees: [{email: 'test@expensify.com'}],
amount: 1000,
currency: 'USD',
cardID: 5,
@@ -90,6 +92,7 @@ DisplayMultipleEReceipts.args = {
thumbnail: '',
transaction: {
transactionID: 'FAKE_5',
+ attendees: [{email: 'test@expensify.com'}],
amount: 230440,
currency: 'USD',
cardID: 4,
@@ -106,6 +109,7 @@ DisplayMultipleEReceipts.args = {
thumbnail: '',
transaction: {
transactionID: 'FAKE_2',
+ attendees: [{email: 'test@expensify.com'}],
amount: 1000,
currency: 'USD',
cardID: 4,
diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts
index 59db6db0a35c..df7002e29562 100644
--- a/src/types/onyx/IOU.ts
+++ b/src/types/onyx/IOU.ts
@@ -169,5 +169,38 @@ type IOU = {
isSplitRequest?: boolean;
};
+/** Model of IOU attendee */
+type Attendee = {
+ /** IOU attendee email */
+ email?: string;
+
+ /** IOU attendee display name */
+ displayName?: string;
+
+ /** IOU attendee avatar url */
+ avatarUrl?: string;
+
+ /** Account ID */
+ accountID?: number;
+
+ /** Text to be displayed in lists (participant display name) */
+ text?: string;
+
+ /** IOU attendee login */
+ login?: string;
+
+ /** Text that IOU attendee display name and login, if available, for searching purposes */
+ searchText?: string;
+
+ /** Is IOU attendee selected in list */
+ selected?: boolean;
+
+ /** The type of IOU report, i.e. split, request, send, track */
+ iouType?: IOUType;
+
+ /** IOU attendee report ID */
+ reportID?: string;
+};
+
export default IOU;
-export type {Participant, Split};
+export type {Participant, Split, Attendee};
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index 15420d03979d..fd94531abb0f 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -362,6 +362,12 @@ type OriginalMessageModifiedExpense = {
/** The ID of moved report */
movedToReportID?: string;
+
+ /** The old stringify list of attendees */
+ oldAttendees?: string;
+
+ /** The stringify list of attendees */
+ attendees?: string;
};
/** Model of `reimbursement queued` report action */
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index d7d10e07f955..4c7e4facd94d 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -3,7 +3,7 @@ import type {IOURequestType} from '@libs/actions/IOU';
import type CONST from '@src/CONST';
import type ONYXKEYS from '@src/ONYXKEYS';
import type CollectionDataSet from '@src/types/utils/CollectionDataSet';
-import type {Participant, Split} from './IOU';
+import type {Attendee, Participant, Split} from './IOU';
import type * as OnyxCommon from './OnyxCommon';
import type {Unit} from './Policy';
import type RecentWaypoint from './RecentWaypoint';
@@ -310,6 +310,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The original transaction amount */
amount: number;
+ /** Selected attendees */
+ attendees?: Attendee[];
+
/** The transaction tax amount */
taxAmount?: number;
@@ -349,6 +352,9 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
/** The edited transaction amount */
modifiedAmount?: number;
+ /** The edited attendees list */
+ modifiedAttendees?: Attendee[];
+
/** The edited transaction date */
modifiedCreated?: string;
@@ -453,7 +459,7 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback<
/** Whether the transaction is linked to a managed card */
managedCard?: boolean;
},
- keyof Comment | keyof TransactionCustomUnit
+ keyof Comment | keyof TransactionCustomUnit | 'attendees'
>;
/** Keys of pending transaction fields */
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index 6a3f0fb9cf12..c2005d221273 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -80,7 +80,7 @@ describe('actions/IOU', () => {
let transactionThread: OnyxEntry;
let transactionThreadCreatedAction: OnyxEntry;
mockFetch?.pause?.();
- IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
return waitForBatchedUpdates()
.then(
() =>
@@ -279,7 +279,7 @@ describe('actions/IOU', () => {
}),
)
.then(() => {
- IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney(chatReport, amount, [], CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
return waitForBatchedUpdates();
})
.then(
@@ -436,6 +436,7 @@ describe('actions/IOU', () => {
};
const existingTransaction: OnyxTypes.Transaction = {
transactionID: NumberUtils.rand64(),
+ attendees: [{email: 'text@expensify.com'}],
amount: 1000,
comment: {
comment: 'Existing transaction',
@@ -482,7 +483,7 @@ describe('actions/IOU', () => {
.then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${existingTransaction.transactionID}`, existingTransaction))
.then(() => {
if (chatReport) {
- IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney(chatReport, amount, [], CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
}
return waitForBatchedUpdates();
})
@@ -622,7 +623,7 @@ describe('actions/IOU', () => {
let transactionThreadReport: OnyxEntry;
let transactionThreadAction: OnyxEntry;
mockFetch?.pause?.();
- IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
return (
waitForBatchedUpdates()
.then(
@@ -982,6 +983,7 @@ describe('actions/IOU', () => {
jest.advanceTimersByTime(200);
const julesExistingTransaction: OnyxEntry = {
transactionID: NumberUtils.rand64(),
+ attendees: [{email: 'text@expensify.com'}],
amount: 1000,
comment: {
comment: 'This is an existing transaction',
@@ -1422,7 +1424,7 @@ describe('actions/IOU', () => {
let createIOUAction: OnyxEntry>;
let payIOUAction: OnyxEntry;
let transaction: OnyxEntry;
- IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', '', RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
return waitForBatchedUpdates()
.then(
() =>
@@ -1622,7 +1624,7 @@ describe('actions/IOU', () => {
let transaction: OnyxEntry;
mockFetch?.pause?.();
- IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
return waitForBatchedUpdates()
.then(() => {
Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID});
@@ -1776,7 +1778,7 @@ describe('actions/IOU', () => {
let iouAction: OnyxEntry>;
let transaction: OnyxEntry;
- IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
return waitForBatchedUpdates()
.then(() => {
Onyx.set(ONYXKEYS.SESSION, {email: RORY_EMAIL, accountID: RORY_ACCOUNT_ID});
@@ -1958,7 +1960,19 @@ describe('actions/IOU', () => {
)
.then(() => {
if (chatReport) {
- IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney(
+ chatReport,
+ amount,
+ [],
+ CONST.CURRENCY.USD,
+ '',
+ merchant,
+ RORY_EMAIL,
+ RORY_ACCOUNT_ID,
+ {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID},
+ comment,
+ {},
+ );
}
return waitForBatchedUpdates();
})
@@ -2071,7 +2085,19 @@ describe('actions/IOU', () => {
)
.then(() => {
if (chatReport) {
- IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, '', merchant, RORY_EMAIL, RORY_ACCOUNT_ID, {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney(
+ chatReport,
+ amount,
+ [],
+ CONST.CURRENCY.USD,
+ '',
+ merchant,
+ RORY_EMAIL,
+ RORY_ACCOUNT_ID,
+ {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID},
+ comment,
+ {},
+ );
}
return waitForBatchedUpdates();
})
@@ -2157,7 +2183,7 @@ describe('actions/IOU', () => {
await TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID);
// When a submit IOU expense is made
- IOU.requestMoney({reportID: ''}, amount, CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment, {});
+ IOU.requestMoney({reportID: ''}, amount, [], CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment, {});
await waitForBatchedUpdates();
// When fetching all reports from Onyx
@@ -2888,7 +2914,7 @@ describe('actions/IOU', () => {
const amount2 = 20000;
const comment2 = 'Send me money please 2';
if (chatReport) {
- IOU.requestMoney(chatReport, amount2, CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment2, {});
+ IOU.requestMoney(chatReport, amount2, [], CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment2, {});
}
await waitForBatchedUpdates();
@@ -3094,6 +3120,7 @@ describe('actions/IOU', () => {
IOU.requestMoney(
chatReport,
amount,
+ [],
CONST.CURRENCY.USD,
'',
merchant,
@@ -3197,6 +3224,7 @@ describe('actions/IOU', () => {
IOU.requestMoney(
chatReport,
amount,
+ [],
CONST.CURRENCY.USD,
'',
merchant,
@@ -3301,6 +3329,7 @@ describe('actions/IOU', () => {
IOU.requestMoney(
chatReport,
amount,
+ [],
CONST.CURRENCY.USD,
'',
merchant,
diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts
index f23259eb568c..6ec8e11d991a 100644
--- a/tests/unit/TransactionUtilsTest.ts
+++ b/tests/unit/TransactionUtilsTest.ts
@@ -1,3 +1,4 @@
+import type {Attendee} from '@src/types/onyx/IOU';
import * as TransactionUtils from '../../src/libs/TransactionUtils';
import type {Transaction} from '../../src/types/onyx';
@@ -6,8 +7,9 @@ function generateTransaction(values: Partial = {}): Transaction {
const amount = 100;
const currency = 'USD';
const comment = '';
+ const attendees: Attendee[] = [];
const created = '2023-10-01';
- const baseValues = TransactionUtils.buildOptimisticTransaction(amount, currency, reportID, comment, created);
+ const baseValues = TransactionUtils.buildOptimisticTransaction(amount, currency, reportID, comment, attendees, created);
return {...baseValues, ...values};
}
diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts
index e5acf41da17e..659240cc0c30 100644
--- a/tests/unit/ViolationUtilsTest.ts
+++ b/tests/unit/ViolationUtilsTest.ts
@@ -38,7 +38,16 @@ describe('getViolationsOnyxData', () => {
let policyCategories: PolicyCategories;
beforeEach(() => {
- transaction = {transactionID: '123', reportID: '1234', amount: 100, comment: {}, created: '2023-07-24 13:46:20', merchant: 'United Airlines', currency: 'USD'};
+ transaction = {
+ transactionID: '123',
+ attendees: [{email: 'text@expensify.com'}],
+ reportID: '1234',
+ amount: 100,
+ comment: {},
+ created: '2023-07-24 13:46:20',
+ merchant: 'United Airlines',
+ currency: 'USD',
+ };
transactionViolations = [];
policy = {requiresTag: false, requiresCategory: false} as Policy;
policyTags = {};
diff --git a/tests/utils/collections/transaction.ts b/tests/utils/collections/transaction.ts
index 25b4a77c66dd..39abdebe6262 100644
--- a/tests/utils/collections/transaction.ts
+++ b/tests/utils/collections/transaction.ts
@@ -18,6 +18,7 @@ export default function createRandomTransaction(index: number): Transaction {
},
},
},
+ attendees: [{email: randWord()}],
created: randPastDate().toISOString(),
currency: CONST.CURRENCY.USD,
merchant: randWord(),