;
-type ActionWithPayload = {
- type: string;
- payload?: P;
-};
-type StateHolder
= {
- state: string;
- payload: P | null;
-};
-type State
= {
- previous: StateHolder
;
- current: StateHolder
;
-};
-
-/**
- * Represents the state machine configuration as a nested record where:
- * - The first level keys are the state names.
- * - The second level keys are the action types valid for that state.
- * - The corresponding values are the next states to transition to when the action is triggered.
- */
-type StateMachine = Record>;
-
-// eslint-disable-next-line @typescript-eslint/unbound-method
-const client = Log.client;
-
-/**
- * A hook that creates a state machine that can be used with Reanimated Worklets.
- * You can transition state from worklet or from the JS thread.
- *
- * State machines are helpful for managing complex UI interactions. We want to transition
- * between states based on user actions. But also we want to ignore some actions
- * when we are in certain states.
- *
- * For example:
- * 1. Initial state is idle. It can react to KEYBOARD_OPEN action.
- * 2. We open emoji picker. It sends EMOJI_PICKER_OPEN action.
- * 2. There is no handling for this action in idle state so we do nothing.
- * 3. We close emoji picker and it sends EMOJI_PICKER_CLOSE action which again does nothing.
- * 4. We open keyboard. It sends KEYBOARD_OPEN action. idle can react to this action
- * by transitioning into keyboardOpen state
- * 5. Our state is keyboardOpen. It can react to KEYBOARD_CLOSE, EMOJI_PICKER_OPEN actions
- * 6. We open emoji picker again. It sends EMOJI_PICKER_OPEN action which transitions our state
- * into emojiPickerOpen state. Now we react only to EMOJI_PICKER_CLOSE action.
- * 7. Before rendering the emoji picker, the app hides the keyboard.
- * It sends KEYBOARD_CLOSE action. But we ignore it since our emojiPickerOpen state can only handle
- * EMOJI_PICKER_CLOSE action. So we write the logic for handling hiding the keyboard,
- * but maintaining the offset based on the keyboard state shared value
- * 7. We close the picker and send EMOJI_PICKER_CLOSE action which transitions us back into keyboardOpen state.
- *
- * State machine object example:
- * const stateMachine = {
- * idle: {
- * KEYBOARD_OPEN: 'keyboardOpen',
- * },
- * keyboardOpen: {
- * KEYBOARD_CLOSE: 'idle',
- * EMOJI_PICKER_OPEN: 'emojiPickerOpen',
- * },
- * emojiPickerOpen: {
- * EMOJI_PICKER_CLOSE: 'keyboardOpen',
- * },
- * }
- *
- * Initial state example:
- * {
- * previous: null,
- * current: {
- * state: 'idle',
- * payload: null,
- * },
- * }
- *
- * @param stateMachine - a state machine object
- * @param initialState - the initial state of the state machine
- * @returns an object containing the current state, a transition function, and a reset function
- */
-function useWorkletStateMachine(stateMachine: StateMachine, initialState: State
) {
- const currentState = useSharedValue(initialState);
-
- const log = useCallback((message: string, params?: P | null) => {
- 'worklet';
-
- if (!DEBUG_MODE) {
- return;
- }
-
- // eslint-disable-next-line @typescript-eslint/unbound-method, @typescript-eslint/restrict-template-expressions
- runOnJS(client)(`[StateMachine] ${message}. Params: ${JSON.stringify(params)}`);
- }, []);
-
- const transitionWorklet = useCallback(
- (action: ActionWithPayload
) => {
- 'worklet';
-
- if (!action) {
- throw new Error('state machine action is required');
- }
-
- const state = currentState.get();
-
- log(`Current STATE: ${state.current.state}`);
- log(`Next ACTION: ${action.type}`, action.payload);
-
- const nextMachine = stateMachine[state.current.state];
-
- if (!nextMachine) {
- log(`No next machine found for state: ${state.current.state}`);
- return;
- }
-
- const nextState = nextMachine[action.type];
-
- if (!nextState) {
- log(`No next state found for action: ${action.type}`);
- return;
- }
-
- let nextPayload;
-
- if (typeof action.payload === 'undefined') {
- // we save previous payload
- nextPayload = state.current.payload;
- } else {
- // we merge previous payload with the new payload
- nextPayload = {
- ...state.current.payload,
- ...action.payload,
- };
- }
-
- log(`Next STATE: ${nextState}`, nextPayload);
-
- currentState.set({
- previous: state.current,
- current: {
- state: nextState,
- payload: nextPayload,
- },
- });
- },
- [currentState, log, stateMachine],
- );
-
- const resetWorklet = useCallback(() => {
- 'worklet';
-
- log('RESET STATE MACHINE');
- // eslint-disable-next-line react-compiler/react-compiler
- currentState.set(initialState);
- }, [currentState, initialState, log]);
-
- const reset = useCallback(() => {
- runOnUI(resetWorklet)();
- }, [resetWorklet]);
-
- const transition = useCallback(
- (action: ActionWithPayload
) => {
- executeOnUIRuntimeSync(transitionWorklet)(action);
- },
- [transitionWorklet],
- );
-
- return {
- currentState,
- transitionWorklet,
- transition,
- reset,
- };
-}
-
-export type {ActionWithPayload, State};
-export default useWorkletStateMachine;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 6a3e16d0aeec..8c2aeabe1146 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -70,6 +70,7 @@ import type {
FeatureNameParams,
FileLimitParams,
FiltersAmountBetweenParams,
+ FirstDayTextParams,
FlightLayoverParams,
FormattedMaxLengthParams,
ForwardedAmountParams,
@@ -89,6 +90,7 @@ import type {
InvalidPropertyParams,
InvalidValueParams,
IssueVirtualCardParams,
+ LastDayTextParams,
LastFourDigitsParams,
LastSyncAccountingParams,
LastSyncDateParams,
@@ -164,6 +166,7 @@ import type {
ToValidateLoginParams,
TransferParams,
TrialStartedTitleParams,
+ TripLengthTextParams,
UnapprovedParams,
UnapproveWithIntegrationWarningParams,
UnshareParams,
@@ -490,6 +493,9 @@ const translations = {
skip: 'Skip',
chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `Need something specific? Chat with your account manager, ${accountManagerDisplayName}.`,
chatNow: 'Chat now',
+ destination: 'Destination',
+ subrate: 'Subrate',
+ perDiem: 'Per diem',
},
supportalNoAccess: {
title: 'Not so fast',
@@ -870,6 +876,7 @@ const translations = {
createExpense: 'Create expense',
trackExpense: 'Track expense',
chooseRecipient: 'Choose recipient',
+ createExpenseWithAmount: ({amount}: {amount: string}) => `Create ${amount} expense`,
confirmDetails: 'Confirm details',
pay: 'Pay',
cancelPayment: 'Cancel payment',
@@ -1017,6 +1024,9 @@ const translations = {
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',
+ invalidQuantity: 'Please enter a valid quantity.',
+ quantityGreaterThanZero: 'Quantity must be greater than zero.',
+ invalidSubrateLength: 'There must be at least one subrate.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`,
enableWallet: 'Enable wallet',
@@ -1075,6 +1085,18 @@ const translations = {
attendees: 'Attendees',
paymentComplete: 'Payment complete',
justTrackIt: 'Just track it (don’t submit it)',
+ time: 'Time',
+ startDate: 'Start date',
+ endDate: 'End date',
+ startTime: 'Start time',
+ endTime: 'End time',
+ deleteSubrate: 'Delete subrate',
+ deleteSubrateConfirmation: 'Are you sure you want to delete this subrate?',
+ quantity: 'Quantity',
+ subrateSelection: 'Select a subrate and enter a quantity.',
+ firstDayText: ({hours}: FirstDayTextParams) => `First day: ${hours} hours`,
+ lastDayText: ({hours}: LastDayTextParams) => `Last day: ${hours} hours`,
+ tripLengthText: ({days}: TripLengthTextParams) => `Trip: ${days} full days`,
},
notificationPreferencesPage: {
header: 'Notification preferences',
@@ -1993,15 +2015,6 @@ const translations = {
ownershipPercentage: 'Please enter a valid percentage number.',
},
},
- addPersonalBankAccount: {
- countrySelectionStepHeader: "Where's your bank account located?",
- accountDetailsStepHeader: 'What are your account details?',
- accountTypeStepHeader: 'What type of account is this?',
- bankInformationStepHeader: 'What are your bank details?',
- accountHolderInformationStepHeader: 'What are the account holder details?',
- howDoWeProtectYourData: 'How do we protect your data?',
- currencyHeader: "What's your bank account's currency?",
- },
addPersonalBankAccountPage: {
enterPassword: 'Enter Expensify password',
alreadyAdded: 'This account has already been added.',
@@ -2546,7 +2559,6 @@ const translations = {
displayedAs: 'Displayed as',
plan: 'Plan',
profile: 'Workspace profile',
- perDiem: 'Per diem',
bankAccount: 'Bank account',
connectBankAccount: 'Connect bank account',
testTransactions: 'Test transactions',
@@ -2557,6 +2569,8 @@ const translations = {
other: (count: number) => `${count} selected`,
}),
settlementFrequency: 'Settlement frequency',
+ setAsDefault: 'Set as default workspace',
+ defaultNote: `Receipts sent to ${CONST.EMAIL.RECEIPTS} will appear in this workspace.`,
deleteConfirmation: 'Are you sure you want to delete this workspace?',
deleteWithCardsConfirmation: 'Are you sure you want to delete this workspace? This will remove all card feeds and assigned cards.',
unavailable: 'Unavailable workspace',
@@ -2618,8 +2632,6 @@ const translations = {
},
perDiem: {
subtitle: 'Set per diem rates to control daily employee spend. ',
- destination: 'Destination',
- subrate: 'Subrate',
amount: 'Amount',
deleteRates: () => ({
one: 'Delete rate',
@@ -2639,6 +2651,7 @@ const translations = {
},
importPerDiemRates: 'Import per diem rates',
editPerDiemRate: 'Edit per diem rate',
+ editPerDiemRates: 'Edit per diem rates',
editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `Updating this destination will change it for all ${destination} per diem subrates.`,
editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `Updating this currency will change it for all ${destination} per diem subrates.`,
},
@@ -4353,8 +4366,6 @@ const translations = {
onlyAvailableOnPlan: 'Per diem are only available on the Control plan, starting at ',
},
pricing: {
- collect: '$5 ',
- amount: '$9 ',
perActiveMember: 'per active member per month.',
},
note: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 244543e33103..7917788c83ad 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -69,6 +69,7 @@ import type {
FeatureNameParams,
FileLimitParams,
FiltersAmountBetweenParams,
+ FirstDayTextParams,
FlightLayoverParams,
FormattedMaxLengthParams,
ForwardedAmountParams,
@@ -88,6 +89,7 @@ import type {
InvalidPropertyParams,
InvalidValueParams,
IssueVirtualCardParams,
+ LastDayTextParams,
LastFourDigitsParams,
LastSyncAccountingParams,
LastSyncDateParams,
@@ -163,6 +165,7 @@ import type {
ToValidateLoginParams,
TransferParams,
TrialStartedTitleParams,
+ TripLengthTextParams,
UnapprovedParams,
UnapproveWithIntegrationWarningParams,
UnshareParams,
@@ -481,6 +484,9 @@ const translations = {
minuteAbbreviation: 'm',
chatWithAccountManager: ({accountManagerDisplayName}: ChatWithAccountManagerParams) => `¿Necesitas algo especÃfico? Habla con tu gerente de cuenta, ${accountManagerDisplayName}.`,
chatNow: 'Chatear ahora',
+ destination: 'Destino',
+ subrate: 'Subtasa',
+ perDiem: 'Per diem',
},
supportalNoAccess: {
title: 'No tan rápido',
@@ -866,6 +872,7 @@ const translations = {
trackExpense: 'Seguimiento de gastos',
chooseRecipient: 'Elige destinatario',
confirmDetails: 'Confirma los detalles',
+ createExpenseWithAmount: ({amount}: {amount: string}) => `Crear un gasto de ${amount}`,
pay: 'Pagar',
cancelPayment: 'Cancelar el pago',
cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?',
@@ -1015,6 +1022,9 @@ const translations = {
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',
+ invalidQuantity: 'Por favor, introduce una cantidad válida.',
+ quantityGreaterThanZero: 'La cantidad debe ser mayor que cero.',
+ invalidSubrateLength: 'Debe haber al menos una subtasa.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su billetera`,
enableWallet: 'Habilitar billetera',
@@ -1073,6 +1083,18 @@ const translations = {
attendees: 'Asistentes',
paymentComplete: 'Pago completo',
justTrackIt: 'Solo guardarlo (no enviarlo)',
+ time: 'Tiempo',
+ startDate: 'Fecha de inicio',
+ endDate: 'Fecha de finalización',
+ startTime: 'Hora de inicio',
+ endTime: 'Hora de finalización',
+ deleteSubrate: 'Eliminar subtasa',
+ deleteSubrateConfirmation: '¿Estás seguro de que deseas eliminar esta subtasa?',
+ quantity: 'Cantidad',
+ subrateSelection: 'Selecciona una subtasa e introduce una cantidad.',
+ firstDayText: ({hours}: FirstDayTextParams) => `Primer dÃa: ${hours} horas`,
+ lastDayText: ({hours}: LastDayTextParams) => `Último dÃa: ${hours} horas`,
+ tripLengthText: ({days}: TripLengthTextParams) => `Viaje: ${days} dÃas completos`,
},
notificationPreferencesPage: {
header: 'Preferencias de avisos',
@@ -2015,15 +2037,6 @@ const translations = {
ownershipPercentage: 'Por favor, ingrese un número de porcentaje válido.',
},
},
- addPersonalBankAccount: {
- countrySelectionStepHeader: '¿Dónde está ubicada tu cuenta bancaria?',
- accountDetailsStepHeader: '¿Cuáles son los detalles de tu cuenta?',
- accountTypeStepHeader: '¿Qué tipo de cuenta es esta?',
- bankInformationStepHeader: '¿Cuáles son los detalles de tu banco?',
- accountHolderInformationStepHeader: '¿Cuáles son los detalles del titular de la cuenta?',
- howDoWeProtectYourData: '¿Cómo protegemos tus datos?',
- currencyHeader: '¿Cuál es la moneda de tu cuenta bancaria?',
- },
addPersonalBankAccountPage: {
enterPassword: 'Escribe tu contraseña de Expensify',
alreadyAdded: 'Esta cuenta ya ha sido añadida.',
@@ -2568,7 +2581,6 @@ const translations = {
rules: 'Reglas',
plan: 'Plan',
profile: 'Perfil del espacio de trabajo',
- perDiem: 'Per diem',
bankAccount: 'Cuenta bancaria',
displayedAs: 'Mostrado como',
connectBankAccount: 'Conectar cuenta bancaria',
@@ -2580,6 +2592,8 @@ const translations = {
other: (count: number) => `${count} seleccionados`,
}),
settlementFrequency: 'Frecuencia de liquidación',
+ setAsDefault: 'Establecer como espacio de trabajo predeterminado',
+ defaultNote: `Los recibos enviados a ${CONST.EMAIL.RECEIPTS} aparecerán en este espacio de trabajo.`,
deleteConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo?',
deleteWithCardsConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo? Se eliminarán todos los datos de las tarjetas y las tarjetas asignadas.',
unavailable: 'Espacio de trabajo no disponible',
@@ -2642,8 +2656,6 @@ const translations = {
},
perDiem: {
subtitle: 'Establece las tasas per diem para controlar los gastos diarios de los empleados. ',
- destination: 'Destino',
- subrate: 'Subtasa',
amount: 'Cantidad',
deleteRates: () => ({
one: 'Eliminar tasa',
@@ -2663,6 +2675,7 @@ const translations = {
},
importPerDiemRates: 'Importar tasas de per diem',
editPerDiemRate: 'Editar la tasa de per diem',
+ editPerDiemRates: 'Editar las tasas de per diem',
editDestinationSubtitle: ({destination}: EditDestinationSubtitleParams) => `Actualizar este destino lo modificará para todas las subtasas per diem de ${destination}.`,
editCurrencySubtitle: ({destination}: EditDestinationSubtitleParams) => `Actualizar esta moneda la modificará para todas las subtasas per diem de ${destination}.`,
},
@@ -4424,8 +4437,6 @@ const translations = {
aboutOurPlans: 'sobre nuestros planes y precios.',
},
pricing: {
- collect: '$5 ',
- amount: '$9 ',
perActiveMember: 'por miembro activo al mes.',
},
upgradeToUnlock: 'Desbloquear esta función',
diff --git a/src/languages/params.ts b/src/languages/params.ts
index f9ca26a3575a..969706eeeb2f 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -586,6 +586,18 @@ type ChatWithAccountManagerParams = {
accountManagerDisplayName: string;
};
+type FirstDayTextParams = {
+ hours: string;
+};
+
+type LastDayTextParams = {
+ hours: string;
+};
+
+type TripLengthTextParams = {
+ days: number;
+};
+
type EditDestinationSubtitleParams = {
destination: string;
};
@@ -802,6 +814,9 @@ export type {
CompanyNameParams,
CustomUnitRateParams,
ChatWithAccountManagerParams,
+ FirstDayTextParams,
+ LastDayTextParams,
+ TripLengthTextParams,
EditDestinationSubtitleParams,
FlightLayoverParams,
};
diff --git a/src/libs/API/parameters/BankAccountCreateCorpayParams.ts b/src/libs/API/parameters/BankAccountCreateCorpayParams.ts
index c753d4c4ffb2..3c617d326009 100644
--- a/src/libs/API/parameters/BankAccountCreateCorpayParams.ts
+++ b/src/libs/API/parameters/BankAccountCreateCorpayParams.ts
@@ -1,5 +1,5 @@
type BankAccountCreateCorpayParams = {
- type?: number;
+ type: number;
isSavings: boolean;
isWithdrawal: boolean;
inputs: string;
diff --git a/src/libs/API/parameters/CompleteGuidedSetupParams.ts b/src/libs/API/parameters/CompleteGuidedSetupParams.ts
index febb1a47d8b6..a70ebbe5e125 100644
--- a/src/libs/API/parameters/CompleteGuidedSetupParams.ts
+++ b/src/libs/API/parameters/CompleteGuidedSetupParams.ts
@@ -11,6 +11,8 @@ type CompleteGuidedSetupParams = {
companySize?: OnboardingCompanySize;
userReportedIntegration?: OnboardingAccounting;
policyID?: string;
+ selfDMReportID?: string;
+ selfDMCreatedReportActionID?: string;
};
export default CompleteGuidedSetupParams;
diff --git a/src/libs/API/parameters/GetCorpayBankAccountFieldsParams.ts b/src/libs/API/parameters/GetCorpayBankAccountFieldsParams.ts
index a1228a023abe..3e02b57f9e12 100644
--- a/src/libs/API/parameters/GetCorpayBankAccountFieldsParams.ts
+++ b/src/libs/API/parameters/GetCorpayBankAccountFieldsParams.ts
@@ -1,8 +1,8 @@
type GetCorpayBankAccountFieldsParams = {
countryISO: string;
- currency?: string;
- isWithdrawal?: boolean;
- isBusinessBankAccount?: boolean;
+ currency: string;
+ isWithdrawal: boolean;
+ isBusinessBankAccount: boolean;
};
export default GetCorpayBankAccountFieldsParams;
diff --git a/src/libs/API/parameters/SetNameValuePairParams.ts b/src/libs/API/parameters/SetNameValuePairParams.ts
index bc83d431224b..e09ea4cae69f 100644
--- a/src/libs/API/parameters/SetNameValuePairParams.ts
+++ b/src/libs/API/parameters/SetNameValuePairParams.ts
@@ -1,6 +1,6 @@
type SetNameValuePairParams = {
name: string;
- value: boolean;
+ value: boolean | string;
};
export default SetNameValuePairParams;
diff --git a/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
index 6ef6b3712439..5b7a221a8702 100644
--- a/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
+++ b/src/libs/API/parameters/VerifyIdentityForBankAccountParams.ts
@@ -1,6 +1,6 @@
type VerifyIdentityForBankAccountParams = {
bankAccountID: number;
onfidoData: string;
- policyID: string;
+ policyID?: string;
};
export default VerifyIdentityForBankAccountParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 7b1f8a203ffc..ea2d9893cf24 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -8,6 +8,7 @@ export type {default as RestartBankAccountSetupParams} from './RestartBankAccoun
export type {default as AddSchoolPrincipalParams} from './AddSchoolPrincipalParams';
export type {default as AuthenticatePusherParams} from './AuthenticatePusherParams';
export type {default as BankAccountHandlePlaidErrorParams} from './BankAccountHandlePlaidErrorParams';
+export type {default as BankAccountCreateCorpayParams} from './BankAccountCreateCorpayParams';
export type {default as BeginAppleSignInParams} from './BeginAppleSignInParams';
export type {default as BeginGoogleSignInParams} from './BeginGoogleSignInParams';
export type {default as BeginSignInParams} from './BeginSignInParams';
@@ -29,6 +30,7 @@ export type {default as ExpandURLPreviewParams} from './ExpandURLPreviewParams';
export type {default as GetMissingOnyxMessagesParams} from './GetMissingOnyxMessagesParams';
export type {default as GetNewerActionsParams} from './GetNewerActionsParams';
export type {default as GetOlderActionsParams} from './GetOlderActionsParams';
+export type {default as GetCorpayBankAccountFieldsParams} from './GetCorpayBankAccountFieldsParams';
export type {default as GetPolicyCategoriesParams} from './GetPolicyCategories';
export type {default as GetReportPrivateNoteParams} from './GetReportPrivateNoteParams';
export type {default as GetRouteParams} from './GetRouteParams';
@@ -353,8 +355,6 @@ export type {default as UpdateQuickbooksDesktopCompanyCardExpenseAccountTypePara
export type {default as TogglePolicyPerDiemParams} from './TogglePolicyPerDiemParams';
export type {default as OpenPolicyPerDiemRatesPageParams} from './OpenPolicyPerDiemRatesPageParams';
export type {default as TogglePlatformMuteParams} from './TogglePlatformMuteParams';
-export type {default as GetCorpayBankAccountFieldsParams} from './GetCorpayBankAccountFieldsParams';
-export type {default as BankAccountCreateCorpayParams} from './BankAccountCreateCorpayParams';
export type {default as JoinAccessiblePolicyParams} from './JoinAccessiblePolicyParams';
export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesParams';
export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 2980e0d822ab..7b8c6df92a22 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -440,7 +440,6 @@ const WRITE_COMMANDS = {
SELF_TOUR_VIEWED: 'SelfTourViewed',
UPDATE_INVOICE_COMPANY_NAME: 'UpdateInvoiceCompanyName',
UPDATE_INVOICE_COMPANY_WEBSITE: 'UpdateInvoiceCompanyWebsite',
- GET_CORPAY_BANK_ACCOUNT_FIELDS: 'GetCorpayBankAccountFields',
BANK_ACCOUNT_CREATE_CORPAY: 'BankAccount_CreateCorpay',
UPDATE_WORKSPACE_CUSTOM_UNIT: 'UpdateWorkspaceCustomUnit',
VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'ValidateUserAndGetAccessiblePolicies',
@@ -770,7 +769,6 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY]: Parameters.UpdateSubscriptionAddNewUsersAutomaticallyParams;
[WRITE_COMMANDS.UPDATE_SUBSCRIPTION_SIZE]: Parameters.UpdateSubscriptionSizeParams;
[WRITE_COMMANDS.REQUEST_TAX_EXEMPTION]: null;
- [WRITE_COMMANDS.GET_CORPAY_BANK_ACCOUNT_FIELDS]: Parameters.GetCorpayBankAccountFieldsParams;
[WRITE_COMMANDS.UPDATE_WORKSPACE_CUSTOM_UNIT]: Parameters.UpdateWorkspaceCustomUnitParams;
[WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams;
@@ -1049,7 +1047,6 @@ const SIDE_EFFECT_REQUEST_COMMANDS = {
DISCONNECT_AS_DELEGATE: 'DisconnectAsDelegate',
COMPLETE_HYBRID_APP_ONBOARDING: 'CompleteHybridAppOnboarding',
CONNECT_POLICY_TO_QUICKBOOKS_DESKTOP: 'ConnectPolicyToQuickbooksDesktop',
- BANK_ACCOUNT_CREATE_CORPAY: 'BankAccount_CreateCorpay',
// PayMoneyRequestOnSearch only works online (pattern C) and we need to play the success sound only when the request is successful
PAY_MONEY_REQUEST_ON_SEARCH: 'PayMoneyRequestOnSearch',
@@ -1073,7 +1070,6 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.DISCONNECT_AS_DELEGATE]: EmptyObject;
[SIDE_EFFECT_REQUEST_COMMANDS.COMPLETE_HYBRID_APP_ONBOARDING]: EmptyObject;
[SIDE_EFFECT_REQUEST_COMMANDS.CONNECT_POLICY_TO_QUICKBOOKS_DESKTOP]: Parameters.ConnectPolicyToQuickBooksDesktopParams;
- [SIDE_EFFECT_REQUEST_COMMANDS.BANK_ACCOUNT_CREATE_CORPAY]: Parameters.BankAccountCreateCorpayParams;
[SIDE_EFFECT_REQUEST_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH]: Parameters.PayMoneyRequestOnSearchParams;
};
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index 5ea3fd605d6f..d9ca27556062 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -442,6 +442,14 @@ function getEndOfToday(): string {
return format(date, 'yyyy-MM-dd HH:mm:ss');
}
+/**
+ * returns {string} example: 2023-05-16 05:34:14
+ */
+function getStartOfToday(): string {
+ const date = startOfDay(new Date());
+ return format(date, 'yyyy-MM-dd HH:mm:ss');
+}
+
/**
* returns {string} example: 2023-05-16 05:34:14
*/
@@ -661,6 +669,17 @@ const isTimeAtLeastOneMinuteInFuture = ({timeString, dateTimeString}: {timeStrin
return isAfter(new Date(dateToCheck), addMinutes(now, 1));
};
+/**
+ * Checks if the time range input is valid.
+ * param {String} startTime: '2023-11-14 12:24:00'
+ * param {String} endTime: '2023-11-14 14:24:00'
+ * returns {Boolean}
+ */
+const isValidStartEndTimeRange = ({startTime, endTime}: {startTime: string; endTime: string}): boolean => {
+ // Check if the combinedDate is at least one minute later than the current date and time
+ return isAfter(new Date(endTime), new Date(startTime));
+};
+
/**
* Checks if the input date is in the future compared to the reference date.
* param {Date} inputDate - The date to validate.
@@ -907,11 +926,13 @@ const DateUtils = {
subtractMillisecondsFromDateTime,
addMillisecondsFromDateTime,
getEndOfToday,
+ getStartOfToday,
getDateFromStatusType,
getOneHourFromNow,
extractDate,
getStatusUntilDate,
extractTime12Hour,
+ formatDateTimeTo12Hour,
get12HourTimeObjectFromDate,
getLocalizedTimePeriodDescription,
combineDateAndTime,
@@ -925,6 +946,7 @@ const DateUtils = {
formatWithUTCTimeZone,
getWeekEndsOn,
isTimeAtLeastOneMinuteInFuture,
+ isValidStartEndTimeRange,
formatToSupportedTimezone,
getLastBusinessDayOfMonth,
getFormattedDateRange,
diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts
index fd8ad40d7501..5824a79532bd 100644
--- a/src/libs/DebugUtils.ts
+++ b/src/libs/DebugUtils.ts
@@ -520,6 +520,8 @@ function validateReportDraftProperty(key: keyof Report, value: string) {
);
case 'errorFields':
return validateObject>(value, {}, 'string');
+ case 'errors':
+ return validateObject>(value, {});
case 'privateNotes':
return validateObject>(
value,
@@ -624,6 +626,7 @@ function validateReportDraftProperty(key: keyof Report, value: string) {
reimbursed: CONST.RED_BRICK_ROAD_PENDING_ACTION,
preview: CONST.RED_BRICK_ROAD_PENDING_ACTION,
welcomeMessage: CONST.RED_BRICK_ROAD_PENDING_ACTION,
+ errors: CONST.RED_BRICK_ROAD_PENDING_ACTION,
});
}
}
@@ -978,6 +981,8 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
return validateObject>(
value,
{
+ attributes: CONST.RED_BRICK_ROAD_PENDING_ACTION,
+ subRates: CONST.RED_BRICK_ROAD_PENDING_ACTION,
comment: CONST.RED_BRICK_ROAD_PENDING_ACTION,
hold: CONST.RED_BRICK_ROAD_PENDING_ACTION,
waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION,
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 058a0844a5ba..e05d316bd8ca 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -100,6 +100,12 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default,
[SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: () => require('../../../../pages/iou/request/step/IOURequestStepAttendees').default,
[SCREENS.MONEY_REQUEST.STEP_UPGRADE]: () => require('../../../../pages/iou/request/step/IOURequestStepUpgrade').default,
+ [SCREENS.MONEY_REQUEST.STEP_DESTINATION]: () => require('../../../../pages/iou/request/step/IOURequestStepDestination').default,
+ [SCREENS.MONEY_REQUEST.STEP_TIME]: () => require('../../../../pages/iou/request/step/IOURequestStepTime').default,
+ [SCREENS.MONEY_REQUEST.STEP_SUBRATE]: () => require('../../../../pages/iou/request/step/IOURequestStepSubrate').default,
+ [SCREENS.MONEY_REQUEST.STEP_DESTINATION_EDIT]: () => require('../../../../pages/iou/request/step/IOURequestStepDestination').default,
+ [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: () => require('../../../../pages/iou/request/step/IOURequestStepTime').default,
+ [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: () => require('../../../../pages/iou/request/step/IOURequestStepSubrate').default,
});
const TravelModalStackNavigator = createModalStackNavigator({
@@ -247,8 +253,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/EnablePayments/EnablePayments').default,
[SCREENS.SETTINGS.WALLET.VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Wallet/VerifyAccountPage').default,
[SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../../pages/settings/Wallet/AddDebitCardPage').default,
- [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../../pages/settings/Wallet/InternationalDepositAccount').default,
- [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default,
+ [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default,
[SCREENS.SETTINGS.PROFILE.STATUS]: () => require('../../../../pages/settings/Profile/CustomStatus/StatusPage').default,
[SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER]: () => require('../../../../pages/settings/Profile/CustomStatus/StatusClearAfterPage').default,
[SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE]: () => require('../../../../pages/settings/Profile/CustomStatus/SetDatePage').default,
diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx
index c9b43498734a..a8e72c2c82ea 100644
--- a/src/libs/Navigation/OnyxTabNavigator.tsx
+++ b/src/libs/Navigation/OnyxTabNavigator.tsx
@@ -41,6 +41,9 @@ type OnyxTabNavigatorProps = ChildrenProps & {
* Together, with the `onActiveTabFocusTrapContainerElementChanged` callback, we can manage the focus trap of the tab navigator in the parent component.
*/
onTabBarFocusTrapContainerElementChanged?: (containerElement: HTMLElement | null) => void;
+
+ /** Whether to show the label when the tab is inactive */
+ shouldShowLabelWhenInactive?: boolean;
};
// eslint-disable-next-line rulesdir/no-inline-named-export
@@ -62,6 +65,7 @@ function OnyxTabNavigator({
onActiveTabFocusTrapContainerElementChanged,
onTabSelected = () => {},
screenListeners,
+ shouldShowLabelWhenInactive = true,
...rest
}: OnyxTabNavigatorProps) {
// Mapping of tab name to focus trap container element
@@ -90,12 +94,13 @@ function OnyxTabNavigator({
return (
);
},
- [onTabBarFocusTrapContainerElementChanged, TabBar],
+ [TabBar, onTabBarFocusTrapContainerElementChanged, shouldShowLabelWhenInactive],
);
// If the selected tab changes, we need to update the focus trap container element of the active tab
diff --git a/src/libs/Navigation/getTopmostBottomTabRoute.ts b/src/libs/Navigation/getTopmostBottomTabRoute.ts
index 48a8d80f4096..231e815a0016 100644
--- a/src/libs/Navigation/getTopmostBottomTabRoute.ts
+++ b/src/libs/Navigation/getTopmostBottomTabRoute.ts
@@ -15,7 +15,7 @@ function getTopmostBottomTabRoute(state: State | undefined):
throw new Error('BottomTabNavigator route have no routes.');
}
- return {name: topmostBottomTabRoute.name as BottomTabName, params: topmostBottomTabRoute.params};
+ return {name: topmostBottomTabRoute.name as BottomTabName, params: topmostBottomTabRoute.params, key: topmostBottomTabRoute.key};
}
export default getTopmostBottomTabRoute;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 9d4d917311e4..28e0a30dcfc8 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -242,10 +242,6 @@ const config: LinkingOptions['config'] = {
path: ROUTES.SETTINGS_ADD_BANK_ACCOUNT,
exact: true,
},
- [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: {
- path: ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT,
- exact: true,
- },
[SCREENS.SETTINGS.PROFILE.PRONOUNS]: {
path: ROUTES.SETTINGS_PRONOUNS,
exact: true,
@@ -1236,6 +1232,11 @@ const config: LinkingOptions['config'] = {
path: ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.route,
exact: true,
},
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'per-diem': {
+ path: ROUTES.MONEY_REQUEST_CREATE_TAB_PER_DIEM.route,
+ exact: true,
+ },
},
},
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: ROUTES.SETTINGS_CATEGORIES_ROOT.route,
@@ -1262,6 +1263,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.route,
[SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: ROUTES.MONEY_REQUEST_ATTENDEE.route,
[SCREENS.MONEY_REQUEST.STEP_UPGRADE]: ROUTES.MONEY_REQUEST_UPGRADE.route,
+ [SCREENS.MONEY_REQUEST.STEP_DESTINATION]: ROUTES.MONEY_REQUEST_STEP_DESTINATION.route,
+ [SCREENS.MONEY_REQUEST.STEP_TIME]: ROUTES.MONEY_REQUEST_STEP_TIME.route,
+ [SCREENS.MONEY_REQUEST.STEP_SUBRATE]: ROUTES.MONEY_REQUEST_STEP_SUBRATE.route,
+ [SCREENS.MONEY_REQUEST.STEP_DESTINATION_EDIT]: ROUTES.MONEY_REQUEST_STEP_DESTINATION_EDIT.route,
+ [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.route,
+ [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: ROUTES.MONEY_REQUEST_STEP_SUBRATE_EDIT.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 7c6c568a5359..2dfbb5ea1e5f 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -175,7 +175,6 @@ type SettingsNavigatorParamList = {
};
[SCREENS.SETTINGS.ADD_DEBIT_CARD]: undefined;
[SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: undefined;
- [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: undefined;
[SCREENS.SETTINGS.PROFILE.STATUS]: undefined;
[SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER]: undefined;
[SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE]: undefined;
@@ -1232,6 +1231,50 @@ type MoneyRequestNavigatorParamList = {
reportID: string;
backTo: Routes;
};
+ [SCREENS.MONEY_REQUEST.STEP_DESTINATION]: {
+ action: IOUAction;
+ iouType: Exclude;
+ transactionID: string;
+ reportID: string;
+ backTo: Routes | undefined;
+ };
+ [SCREENS.MONEY_REQUEST.STEP_TIME]: {
+ action: IOUAction;
+ iouType: Exclude;
+ transactionID: string;
+ reportID: string;
+ backTo: Routes | undefined;
+ };
+ [SCREENS.MONEY_REQUEST.STEP_SUBRATE]: {
+ iouType: Exclude;
+ reportID: string;
+ backTo: Routes | undefined;
+ action: IOUAction;
+ pageIndex: string;
+ transactionID: string;
+ };
+ [SCREENS.MONEY_REQUEST.STEP_DESTINATION_EDIT]: {
+ action: IOUAction;
+ iouType: Exclude;
+ transactionID: string;
+ reportID: string;
+ backTo: Routes | undefined;
+ };
+ [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: {
+ action: IOUAction;
+ iouType: Exclude;
+ transactionID: string;
+ reportID: string;
+ backTo: Routes | undefined;
+ };
+ [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: {
+ iouType: Exclude;
+ reportID: string;
+ backTo: Routes | undefined;
+ action: IOUAction;
+ pageIndex: string;
+ transactionID: string;
+ };
};
type NewTaskNavigatorParamList = {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index b588b3dd5359..eea0c46f3712 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -108,6 +108,8 @@ type GetOptionsConfig = {
action?: IOUAction;
recentAttendees?: Attendee[];
shouldBoldTitleByDefault?: boolean;
+ shouldSeparateWorkspaceChat?: boolean;
+ shouldSeparateSelfDMChat?: boolean;
};
type GetUserToInviteConfig = {
@@ -138,6 +140,8 @@ type Options = {
personalDetails: ReportUtils.OptionData[];
userToInvite: ReportUtils.OptionData | null;
currentUserOption: ReportUtils.OptionData | null | undefined;
+ workspaceChats?: ReportUtils.OptionData[];
+ selfDMChat?: ReportUtils.OptionData | undefined;
};
type PreviewConfig = {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean; showPersonalDetails?: boolean};
@@ -165,7 +169,7 @@ type OrderReportOptionsConfig = {
preferRecentExpenseReports?: boolean;
};
-type ReportAndPersonalDetailOptions = Pick;
+type ReportAndPersonalDetailOptions = Pick;
/**
* OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can
@@ -1001,6 +1005,23 @@ function orderReportOptionsWithSearch(
);
}
+function orderWorkspaceOptions(options: ReportUtils.OptionData[]): ReportUtils.OptionData[] {
+ return lodashOrderBy(
+ options,
+ [
+ (option) => {
+ // Put default workspace on top
+ if (option.isPolicyExpenseChat && option.policyID === activePolicyID) {
+ return 0;
+ }
+
+ return 1;
+ },
+ ],
+ ['asc'],
+ );
+}
+
function sortComparatorReportOptionByArchivedStatus(option: ReportUtils.OptionData) {
return option.private_isArchived ? 1 : 0;
}
@@ -1028,10 +1049,12 @@ function orderOptions(options: ReportAndPersonalDetailOptions, searchValue?: str
orderedReportOptions = orderReportOptions(options.recentReports);
}
const orderedPersonalDetailsOptions = orderPersonalDetailsOptions(options.personalDetails);
+ const orderedWorkspaceChats = orderWorkspaceOptions(options?.workspaceChats ?? []);
return {
recentReports: orderedReportOptions,
personalDetails: orderedPersonalDetailsOptions,
+ workspaceChats: orderedWorkspaceChats,
};
}
@@ -1140,6 +1163,8 @@ function getValidOptions(
action,
recentAttendees,
shouldBoldTitleByDefault = true,
+ shouldSeparateSelfDMChat = false,
+ shouldSeparateWorkspaceChat = false,
}: GetOptionsConfig = {},
): Options {
const topmostReportId = Navigation.getTopmostReportId() ?? '-1';
@@ -1215,6 +1240,12 @@ function getValidOptions(
return true;
});
+ let workspaceChats: ReportUtils.OptionData[] = [];
+
+ if (shouldSeparateWorkspaceChat) {
+ workspaceChats = allReportOptions.filter((option) => option.isOwnPolicyExpenseChat && !option.private_isArchived);
+ }
+
const allPersonalDetailsOptions = includeP2P
? options.personalDetails.filter((detail) => !!detail?.login && !!detail.accountID && !detail?.isOptimisticPersonalDetail && (includeDomainEmail || !Str.isDomainEmail(detail.login)))
: [];
@@ -1300,7 +1331,7 @@ function getValidOptions(
if (reportPreviewAction) {
const iouReportID = ReportActionUtils.getIOUReportIDFromReportActionPreview(reportPreviewAction);
- const iouReportActions = allSortedReportActions[iouReportID] ?? [];
+ const iouReportActions = iouReportID ? allSortedReportActions[iouReportID] ?? [] : [];
const lastIOUAction = iouReportActions.find((iouAction) => iouAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
if (lastIOUAction) {
reportOption.lastIOUCreationDate = lastIOUAction.lastModified;
@@ -1325,6 +1356,15 @@ function getValidOptions(
}
const currentUserOption = allPersonalDetailsOptions.find((personalDetailsOption) => personalDetailsOption.login === currentUserLogin);
+ let selfDMChat: ReportUtils.OptionData | undefined;
+
+ if (shouldSeparateWorkspaceChat) {
+ recentReportOptions = recentReportOptions.filter((option) => !option.isPolicyExpenseChat);
+ }
+ if (shouldSeparateSelfDMChat) {
+ selfDMChat = recentReportOptions.find((option) => option.isSelfDM);
+ recentReportOptions = recentReportOptions.filter((option) => !option.isSelfDM);
+ }
return {
personalDetails: personalDetailsOptions,
@@ -1333,6 +1373,8 @@ function getValidOptions(
// User to invite is generated by the search input of a user.
// As this function isn't concerned with any search input yet, this is null (will be set when using filterOptions).
userToInvite: null,
+ workspaceChats,
+ selfDMChat,
};
}
@@ -1573,6 +1615,7 @@ function formatSectionsFromSearchTerm(
filteredPersonalDetails: ReportUtils.OptionData[],
personalDetails: OnyxEntry = {},
shouldGetOptionDetails = false,
+ filteredWorkspaceChats: ReportUtils.OptionData[] = [],
): SectionForSearchTerm {
// We show the selected participants at the top of the list when there is no search term or maximum number of participants has already been selected
// However, if there is a search term we remove the selected participants from the top of the list unless they are part of the search results
@@ -1598,8 +1641,9 @@ function formatSectionsFromSearchTerm(
const selectedParticipantsWithoutDetails = selectedOptions.filter((participant) => {
const accountID = participant.accountID ?? null;
const isPartOfSearchTerm = getPersonalDetailSearchTerms(participant).join(' ').toLowerCase().includes(cleanSearchTerm);
- const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID);
+ const isReportInRecentReports = filteredRecentReports.some((report) => report.accountID === accountID) || filteredWorkspaceChats.some((report) => report.accountID === accountID);
const isReportInPersonalDetails = filteredPersonalDetails.some((personalDetail) => personalDetail.accountID === accountID);
+
return isPartOfSearchTerm && !isReportInRecentReports && !isReportInPersonalDetails;
});
@@ -1685,6 +1729,23 @@ function filterReports(reports: ReportUtils.OptionData[], searchTerms: string[])
return filteredReports;
}
+function filterWorkspaceChats(reports: ReportUtils.OptionData[], searchTerms: string[]): ReportUtils.OptionData[] {
+ const filteredReports = searchTerms.reduceRight(
+ (items, term) =>
+ filterArrayByMatch(items, term, (item) => {
+ const values: string[] = [];
+ if (item.text) {
+ values.push(item.text);
+ }
+ return uniqFast(values);
+ }),
+ // We start from all unfiltered reports:
+ reports,
+ );
+
+ return filteredReports;
+}
+
function filterPersonalDetails(personalDetails: ReportUtils.OptionData[], searchTerms: string[]): ReportUtils.OptionData[] {
return searchTerms.reduceRight(
(items, term) =>
@@ -1736,6 +1797,34 @@ function filterUserToInvite(options: Omit, searchValue:
});
}
+function filterSelfDMChat(report: ReportUtils.OptionData, searchTerms: string[]): ReportUtils.OptionData | undefined {
+ const isMatch = searchTerms.every((term) => {
+ const values: string[] = [];
+
+ if (report.text) {
+ values.push(report.text);
+ }
+ if (report.login) {
+ values.push(report.login);
+ values.push(report.login.replace(CONST.EMAIL_SEARCH_REGEX, ''));
+ }
+ if (report.isThread) {
+ if (report.alternateText) {
+ values.push(report.alternateText);
+ }
+ } else if (!!report.isChatRoom || !!report.isPolicyExpenseChat) {
+ if (report.subtitle) {
+ values.push(report.subtitle);
+ }
+ }
+
+ // Remove duplicate values and check if the term matches any value
+ return uniqFast(values).some((value) => value.includes(term));
+ });
+
+ return isMatch ? report : undefined;
+}
+
function filterOptions(options: Options, searchInputValue: string, config?: FilterUserToInviteConfig): Options {
const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue)));
const searchValue = parsedPhoneNumber.possible && parsedPhoneNumber.number?.e164 ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase();
@@ -1753,12 +1842,17 @@ function filterOptions(options: Options, searchInputValue: string, config?: Filt
searchValue,
config,
);
+ const workspaceChats = filterWorkspaceChats(options.workspaceChats ?? [], searchTerms);
+
+ const selfDMChat = options.selfDMChat ? filterSelfDMChat(options.selfDMChat, searchTerms) : undefined;
return {
personalDetails,
recentReports,
userToInvite,
currentUserOption,
+ workspaceChats,
+ selfDMChat,
};
}
diff --git a/src/libs/PaymentUtils.ts b/src/libs/PaymentUtils.ts
index 95fc334b906c..c18ebd217406 100644
--- a/src/libs/PaymentUtils.ts
+++ b/src/libs/PaymentUtils.ts
@@ -27,10 +27,10 @@ function hasExpensifyPaymentMethod(fundList: Record, bankAccountLi
return validBankAccount || (shouldIncludeDebitCard && validDebitCard);
}
-function getPaymentMethodDescription(accountType: AccountType, account: BankAccount['accountData'] | Fund['accountData'] | ACHAccount, bankCurrency?: string): string {
+function getPaymentMethodDescription(accountType: AccountType, account: BankAccount['accountData'] | Fund['accountData'] | ACHAccount): string {
if (account) {
if (accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && 'accountNumber' in account) {
- return `${bankCurrency} ${CONST.DOT_SEPARATOR} ${Localize.translateLocal('paymentMethodList.accountLastFour')} ${account.accountNumber?.slice(-4)}`;
+ return `${Localize.translateLocal('paymentMethodList.accountLastFour')} ${account.accountNumber?.slice(-4)}`;
}
if (accountType === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT && 'accountNumber' in account) {
return `${Localize.translateLocal('paymentMethodList.accountLastFour')} ${account.accountNumber?.slice(-4)}`;
@@ -61,7 +61,7 @@ function formatPaymentMethods(bankAccountList: Record, fund
});
combinedPaymentMethods.push({
...bankAccount,
- description: getPaymentMethodDescription(bankAccount?.accountType, bankAccount.accountData, bankAccount.bankCurrency),
+ description: getPaymentMethodDescription(bankAccount?.accountType, bankAccount.accountData),
icon,
iconSize,
iconHeight,
diff --git a/src/libs/PerDiemRequestUtils.ts b/src/libs/PerDiemRequestUtils.ts
new file mode 100644
index 000000000000..ad82bc606716
--- /dev/null
+++ b/src/libs/PerDiemRequestUtils.ts
@@ -0,0 +1,269 @@
+import {addDays, differenceInDays, differenceInMinutes, format, startOfDay} from 'date-fns';
+import lodashSortBy from 'lodash/sortBy';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import Onyx from 'react-native-onyx';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report, Transaction} from '@src/types/onyx';
+import type {CustomUnit, Rate} from '@src/types/onyx/Policy';
+import * as Localize from './Localize';
+import type {OptionTree, SectionBase} from './OptionsListUtils';
+import * as PolicyUtils from './PolicyUtils';
+import * as ReportUtils from './ReportUtils';
+
+let allReports: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ allReports = value;
+ },
+});
+
+/**
+ * Returns custom unit ID for the per diem transaction
+ */
+function getCustomUnitID(reportID: string) {
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
+ const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`];
+ const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID);
+ let customUnitID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID;
+ let category: string | undefined;
+
+ if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) {
+ const perDiemUnit = Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_PER_DIEM_INTERNATIONAL);
+ if (perDiemUnit) {
+ customUnitID = perDiemUnit.customUnitID;
+ category = perDiemUnit.defaultCategory;
+ }
+ }
+ return {customUnitID, category};
+}
+
+type ModifiedOptionTree = OptionTree & {
+ currency: string;
+};
+
+type DestinationTreeSection = SectionBase & {
+ data: ModifiedOptionTree[];
+ indexOffset?: number;
+};
+
+type Destination = {
+ name: string;
+ rateID: string;
+ currency: string;
+ isSelected?: boolean;
+};
+
+/**
+ * Builds the options for the category tree hierarchy via indents
+ *
+ * @param options - an initial object array
+ * @param options[].enabled - a flag to enable/disable option in a list
+ * @param options[].name - a name of an option
+ * @param options[].rateID - a rateID of an option
+ */
+function getDestinationOptionTree(options: Destination[]): ModifiedOptionTree[] {
+ const optionCollection = new Map();
+ Object.values(options).forEach((option) => {
+ if (optionCollection.has(option.rateID)) {
+ return;
+ }
+
+ optionCollection.set(option.rateID, {
+ text: option.name,
+ keyForList: option.rateID,
+ searchText: option.name,
+ tooltipText: option.name,
+ isDisabled: false,
+ isSelected: !!option.isSelected,
+ currency: option.currency,
+ });
+ });
+
+ return Array.from(optionCollection.values());
+}
+
+/**
+ * Builds the section list for destinations
+ */
+function getDestinationListSections({
+ destinations,
+ searchValue,
+ selectedOptions = [],
+ recentlyUsedDestinations = [],
+ maxRecentReportsToShow = CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW,
+}: {
+ destinations: Rate[];
+ selectedOptions?: Destination[];
+ searchValue?: string;
+ recentlyUsedDestinations?: string[];
+ maxRecentReportsToShow?: number;
+}): DestinationTreeSection[] {
+ const sortedDestinations: Destination[] = lodashSortBy(destinations, 'name').map((rate) => ({
+ name: rate.name ?? '',
+ rateID: rate.customUnitRateID,
+ currency: rate.currency ?? CONST.CURRENCY.USD,
+ }));
+ const destinationSections: DestinationTreeSection[] = [];
+
+ if (searchValue) {
+ const searchDestinations: Destination[] = [];
+
+ sortedDestinations.forEach((destination) => {
+ if (!destination.name.toLowerCase().includes(searchValue.toLowerCase())) {
+ return;
+ }
+ searchDestinations.push({
+ ...destination,
+ isSelected: selectedOptions.some((selectedOption) => selectedOption.rateID === destination.rateID),
+ });
+ });
+
+ const data = getDestinationOptionTree(searchDestinations);
+ destinationSections.push({
+ // "Search" section
+ title: '',
+ shouldShow: true,
+ data,
+ indexOffset: data.length,
+ });
+
+ return destinationSections;
+ }
+
+ if (selectedOptions.length > 0) {
+ const data = getDestinationOptionTree(selectedOptions);
+ destinationSections.push({
+ // "Selected" section
+ title: '',
+ shouldShow: false,
+ data,
+ indexOffset: data.length,
+ });
+ }
+
+ const selectedOptionRateID = selectedOptions.map((selectedOption) => selectedOption.rateID);
+
+ if (sortedDestinations.length < CONST.STANDARD_LIST_ITEM_LIMIT) {
+ const data = getDestinationOptionTree(sortedDestinations);
+ destinationSections.push({
+ // "All" section when items amount less than the threshold
+ title: '',
+ shouldShow: false,
+ data,
+ indexOffset: data.length,
+ });
+
+ return destinationSections;
+ }
+
+ const filteredRecentlyUsedDestinations = sortedDestinations.filter(({rateID}) => recentlyUsedDestinations.includes(rateID) && !selectedOptionRateID.includes(rateID));
+
+ if (filteredRecentlyUsedDestinations.length > 0) {
+ const cutRecentlyUsedDestinations = filteredRecentlyUsedDestinations.slice(0, maxRecentReportsToShow);
+
+ const data = getDestinationOptionTree(cutRecentlyUsedDestinations);
+ destinationSections.push({
+ // "Recent" section
+ title: Localize.translateLocal('common.recent'),
+ shouldShow: true,
+ data,
+ indexOffset: data.length,
+ });
+ }
+
+ const data = getDestinationOptionTree(sortedDestinations);
+ destinationSections.push({
+ // "All" section when items amount more than the threshold
+ title: Localize.translateLocal('common.all'),
+ shouldShow: true,
+ data,
+ indexOffset: data.length,
+ });
+
+ return destinationSections;
+}
+
+function getDestinationForDisplay(customUnit: CustomUnit | undefined, transaction: OnyxEntry) {
+ const customUnitRateID = transaction?.comment?.customUnit?.customUnitRateID;
+ if (!customUnitRateID) {
+ return '';
+ }
+ const selectedDestination = customUnit?.rates?.[customUnitRateID];
+ return selectedDestination?.name ?? '';
+}
+
+function getSubratesFields(customUnit: CustomUnit | undefined, transaction: OnyxEntry) {
+ const customUnitRateID = transaction?.comment?.customUnit?.customUnitRateID;
+ if (!customUnitRateID) {
+ return [];
+ }
+ const selectedDestination = customUnit?.rates?.[customUnitRateID];
+ const countSubrates = selectedDestination?.subRates?.length ?? 0;
+ const currentSubrates = transaction?.comment?.customUnit?.subRates ?? [];
+ if (currentSubrates.length === countSubrates) {
+ return currentSubrates;
+ }
+ return [...currentSubrates, undefined];
+}
+
+type Subrate = {
+ /** Key of the sub rate */
+ key?: string;
+
+ /** ID of the custom unit sub rate */
+ id: string;
+
+ /** Custom unit amount */
+ quantity: number;
+
+ /** Custom unit name */
+ name: string;
+
+ /** Custom unit rate */
+ rate?: number;
+};
+
+function getSubratesForDisplay(subrate: Subrate | undefined) {
+ if (!subrate) {
+ return undefined;
+ }
+ return `${subrate.name}, Qty: ${subrate.quantity}`;
+}
+
+/**
+ * param {string} dateTimeString
+ * returns {string} example: 2023-05-16 11:10 PM
+ */
+function formatDateTimeTo12Hour(dateTimeString: string): string {
+ if (!dateTimeString) {
+ return '';
+ }
+ const date = new Date(dateTimeString);
+ return format(date, 'hh:mm a, yyyy-MM-dd');
+}
+
+function getTimeForDisplay(transaction: OnyxEntry) {
+ const customUnitRateDate = transaction?.comment?.customUnit?.attributes?.dates ?? {start: '', end: ''};
+ return `${formatDateTimeTo12Hour(customUnitRateDate.start)} - ${formatDateTimeTo12Hour(customUnitRateDate.end)}`;
+}
+
+function getTimeDifferenceIntervals(transaction: OnyxEntry) {
+ const customUnitRateDate = transaction?.comment?.customUnit?.attributes?.dates ?? {start: '', end: ''};
+ const startDate = new Date(customUnitRateDate.start);
+ const endDate = new Date(customUnitRateDate.end);
+ const firstDayDiff = differenceInMinutes(startOfDay(addDays(startDate, 1)), startDate);
+ const tripDaysDiff = differenceInDays(startOfDay(endDate), startOfDay(addDays(startDate, 1)));
+ const lastDayDiff = differenceInMinutes(endDate, startOfDay(endDate));
+ return {
+ firstDay: firstDayDiff === 1440 ? undefined : (firstDayDiff / 60).toFixed(2),
+ tripDays: firstDayDiff === 1440 ? tripDaysDiff + 1 : tripDaysDiff,
+ lastDay: lastDayDiff === 0 ? undefined : (lastDayDiff / 60).toFixed(2),
+ };
+}
+
+export type {Destination};
+
+export {getCustomUnitID, getDestinationListSections, getDestinationForDisplay, getSubratesFields, getSubratesForDisplay, getTimeForDisplay, getTimeDifferenceIntervals};
diff --git a/src/libs/Performance.tsx b/src/libs/Performance.tsx
index ef2b08e47229..df642e9f0681 100644
--- a/src/libs/Performance.tsx
+++ b/src/libs/Performance.tsx
@@ -3,51 +3,13 @@ import isObject from 'lodash/isObject';
import lodashTransform from 'lodash/transform';
import React, {forwardRef, Profiler} from 'react';
import {Alert, InteractionManager} from 'react-native';
-import type {PerformanceEntry, PerformanceMark, PerformanceMeasure, ReactNativePerformance, Performance as RNPerformance} from 'react-native-performance';
-import type {PerformanceObserverEntryList} from 'react-native-performance/lib/typescript/performance-observer';
+import performance, {PerformanceObserver, setResourceLoggingEnabled} from 'react-native-performance';
+import type {PerformanceEntry, PerformanceMark, PerformanceMeasure} from 'react-native-performance';
import CONST from '@src/CONST';
import isE2ETestSession from './E2E/isE2ETestSession';
import getComponentDisplayName from './getComponentDisplayName';
import * as Metrics from './Metrics';
-type WrappedComponentConfig = {id: string};
-
-type PerformanceEntriesCallback = (entry: PerformanceEntry) => void;
-
-type Phase = 'mount' | 'update' | 'nested-update';
-
-type WithRenderTraceHOC = >(WrappedComponent: React.ComponentType
) => React.ComponentType
>;
-
-type BlankHOC =
>(Component: React.ComponentType
) => React.ComponentType
;
-
-type SetupPerformanceObserver = () => void;
-type DiffObject = (object: Record, base: Record) => Record;
-type GetPerformanceMetrics = () => PerformanceEntry[];
-type PrintPerformanceMetrics = () => void;
-type MarkStart = (name: string, detail?: Record) => PerformanceMark | void;
-type MarkEnd = (name: string, detail?: Record) => PerformanceMark | void;
-type MeasureFailSafe = (measureName: string, startOrMeasureOptions: string, endMark?: string) => void;
-type MeasureTTI = (endMark?: string) => void;
-type TraceRender = (id: string, phase: Phase, actualDuration: number, baseDuration: number, startTime: number, commitTime: number, interactions: Set) => PerformanceMeasure | void;
-type WithRenderTrace = ({id}: WrappedComponentConfig) => WithRenderTraceHOC | BlankHOC;
-type SubscribeToMeasurements = (callback: PerformanceEntriesCallback) => void;
-
-type PerformanceModule = {
- diffObject: DiffObject;
- setupPerformanceObserver: SetupPerformanceObserver;
- getPerformanceMetrics: GetPerformanceMetrics;
- printPerformanceMetrics: PrintPerformanceMetrics;
- markStart: MarkStart;
- markEnd: MarkEnd;
- measureFailSafe: MeasureFailSafe;
- measureTTI: MeasureTTI;
- traceRender: TraceRender;
- withRenderTrace: WithRenderTrace;
- subscribeToMeasurements: SubscribeToMeasurements;
-};
-
-let rnPerformance: RNPerformance;
-
/**
* Deep diff between two objects. Useful for figuring out what changed about an object from one render to the next so
* that state and props updates can be optimized.
@@ -66,204 +28,242 @@ function diffObject(object: Record, base: Record {},
- getPerformanceMetrics: () => [],
- printPerformanceMetrics: () => {},
- markStart: () => {},
- markEnd: () => {},
- measureFailSafe: () => {},
- measureTTI: () => {},
- traceRender: () => {},
- withRenderTrace:
- () =>
- // eslint-disable-next-line @typescript-eslint/naming-convention
- >(Component: React.ComponentType
): React.ComponentType
=>
- Component,
- subscribeToMeasurements: () => {},
-};
+function measureFailSafe(measureName: string, startOrMeasureOptions: string, endMark?: string): void {
+ try {
+ performance.measure(measureName, startOrMeasureOptions, endMark);
+ } catch (error) {
+ // Sometimes there might be no start mark recorded and the measure will fail with an error
+ if (error instanceof Error) {
+ console.debug(error.message);
+ }
+ }
+}
+
+/**
+ * Measures the TTI (time to interactive) time starting from the `nativeLaunchStart` event.
+ * To be called when the app is considered to be interactive.
+ */
+function measureTTI(endMark?: string): void {
+ // Make sure TTI is captured when the app is really usable
+ InteractionManager.runAfterInteractions(() => {
+ requestAnimationFrame(() => {
+ measureFailSafe('TTI', 'nativeLaunchStart', endMark);
-if (Metrics.canCapturePerformanceMetrics()) {
- const perfModule = require('react-native-performance');
- perfModule.setResourceLoggingEnabled(true);
- rnPerformance = perfModule.default;
-
- Performance.measureFailSafe = (measureName: string, startOrMeasureOptions: string, endMark?: string) => {
- try {
- rnPerformance.measure(measureName, startOrMeasureOptions, endMark);
- } catch (error) {
- // Sometimes there might be no start mark recorded and the measure will fail with an error
- if (error instanceof Error) {
- console.debug(error.message);
+ // We don't want an alert to show:
+ // - on builds with performance metrics collection disabled by a feature flag
+ // - e2e test sessions
+ if (!Metrics.canCapturePerformanceMetrics() || isE2ETestSession()) {
+ return;
}
- }
- };
- /**
- * Measures the TTI time. To be called when the app is considered to be interactive.
- */
- Performance.measureTTI = (endMark?: string) => {
- // Make sure TTI is captured when the app is really usable
- InteractionManager.runAfterInteractions(() => {
- requestAnimationFrame(() => {
- Performance.measureFailSafe('TTI', 'nativeLaunchStart', endMark);
-
- // we don't want the alert to show on an e2e test session
- if (!isE2ETestSession()) {
- Performance.printPerformanceMetrics();
- }
- });
+ printPerformanceMetrics();
});
- };
+ });
+}
- /**
- * Sets up an observer to capture events recorded in the native layer before the app fully initializes.
- */
- Performance.setupPerformanceObserver = () => {
- // Monitor some native marks that we want to put on the timeline
- new perfModule.PerformanceObserver((list: PerformanceObserverEntryList, observer: PerformanceObserver) => {
- list.getEntries().forEach((entry: PerformanceEntry) => {
- if (entry.name === 'nativeLaunchEnd') {
- Performance.measureFailSafe('nativeLaunch', 'nativeLaunchStart', 'nativeLaunchEnd');
- }
- if (entry.name === 'downloadEnd') {
- Performance.measureFailSafe('jsBundleDownload', 'downloadStart', 'downloadEnd');
- }
- if (entry.name === 'runJsBundleEnd') {
- Performance.measureFailSafe('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd');
- }
- if (entry.name === 'appCreationEnd') {
- Performance.measureFailSafe('appCreation', 'appCreationStart', 'appCreationEnd');
- Performance.measureFailSafe('nativeLaunchEnd_To_appCreationStart', 'nativeLaunchEnd', 'appCreationStart');
- }
- if (entry.name === 'contentAppeared') {
- Performance.measureFailSafe('appCreationEnd_To_contentAppeared', 'appCreationEnd', 'contentAppeared');
- }
-
- // We don't need to keep the observer past this point
- if (entry.name === 'runJsBundleEnd' || entry.name === 'downloadEnd') {
- observer.disconnect();
- }
- });
- }).observe({type: 'react-native-mark', buffered: true});
-
- // Monitor for "_end" marks and capture "_start" to "_end" measures
- new perfModule.PerformanceObserver((list: PerformanceObserverEntryList) => {
- list.getEntriesByType('mark').forEach((mark: PerformanceEntry) => {
- if (mark.name.endsWith('_end')) {
- const end = mark.name;
- const name = end.replace(/_end$/, '');
- const start = `${name}_start`;
- Performance.measureFailSafe(name, start, end);
- }
-
- // Capture any custom measures or metrics below
- if (mark.name === `${CONST.TIMING.SIDEBAR_LOADED}_end`) {
- Performance.measureFailSafe('contentAppeared_To_screenTTI', 'contentAppeared', mark.name);
- Performance.measureTTI(mark.name);
- }
- });
- }).observe({type: 'mark', buffered: true});
- };
+/*
+ * Monitor native marks that we want to put on the timeline
+ */
+const nativeMarksObserver = new PerformanceObserver((list, _observer) => {
+ list.getEntries().forEach((entry) => {
+ if (entry.name === 'nativeLaunchEnd') {
+ measureFailSafe('nativeLaunch', 'nativeLaunchStart', 'nativeLaunchEnd');
+ }
+ if (entry.name === 'downloadEnd') {
+ measureFailSafe('jsBundleDownload', 'downloadStart', 'downloadEnd');
+ }
+ if (entry.name === 'runJsBundleEnd') {
+ measureFailSafe('runJsBundle', 'runJsBundleStart', 'runJsBundleEnd');
+ }
+ if (entry.name === 'appCreationEnd') {
+ measureFailSafe('appCreation', 'appCreationStart', 'appCreationEnd');
+ measureFailSafe('nativeLaunchEnd_To_appCreationStart', 'nativeLaunchEnd', 'appCreationStart');
+ }
+ if (entry.name === 'contentAppeared') {
+ measureFailSafe('appCreationEnd_To_contentAppeared', 'appCreationEnd', 'contentAppeared');
+ }
- Performance.getPerformanceMetrics = (): PerformanceEntry[] =>
- [
- ...rnPerformance.getEntriesByName('nativeLaunch'),
- ...rnPerformance.getEntriesByName('nativeLaunchEnd_To_appCreationStart'),
- ...rnPerformance.getEntriesByName('appCreation'),
- ...rnPerformance.getEntriesByName('appCreationEnd_To_contentAppeared'),
- ...rnPerformance.getEntriesByName('contentAppeared_To_screenTTI'),
- ...rnPerformance.getEntriesByName('runJsBundle'),
- ...rnPerformance.getEntriesByName('jsBundleDownload'),
- ...rnPerformance.getEntriesByName('TTI'),
- ...rnPerformance.getEntriesByName('regularAppStart'),
- ...rnPerformance.getEntriesByName('appStartedToReady'),
- ].filter((entry) => entry.duration > 0);
-
- /**
- * Outputs performance stats. We alert these so that they are easy to access in release builds.
- */
- Performance.printPerformanceMetrics = () => {
- const stats = Performance.getPerformanceMetrics();
- const statsAsText = stats.map((entry) => `\u2022 ${entry.name}: ${entry.duration.toFixed(1)}ms`).join('\n');
-
- if (stats.length > 0) {
- Alert.alert('Performance', statsAsText);
+ // At this point we've captured and processed all the native marks we're interested in
+ // and are not expecting to have more thus we can safely disconnect the observer
+ if (entry.name === 'runJsBundleEnd' || entry.name === 'downloadEnd') {
+ _observer.disconnect();
}
- };
+ });
+});
- Performance.subscribeToMeasurements = (callback: PerformanceEntriesCallback) => {
- new perfModule.PerformanceObserver((list: PerformanceObserverEntryList) => {
- list.getEntriesByType('measure').forEach(callback);
- }).observe({type: 'measure', buffered: true});
+function setNativeMarksObserverEnabled(enabled = false): void {
+ if (!enabled) {
+ nativeMarksObserver.disconnect();
+ return;
+ }
+
+ nativeMarksObserver.disconnect();
+ nativeMarksObserver.observe({type: 'react-native-mark', buffered: true});
+}
+
+/**
+ * Monitor for "_end" marks and capture "_start" to "_end" measures, including events recorded in the native layer before the app fully initializes.
+ */
+const customMarksObserver = new PerformanceObserver((list) => {
+ list.getEntriesByType('mark').forEach((mark) => {
+ if (mark.name.endsWith('_end')) {
+ const end = mark.name;
+ const name = end.replace(/_end$/, '');
+ const start = `${name}_start`;
+ measureFailSafe(name, start, end);
+ }
+
+ // Capture any custom measures or metrics below
+ if (mark.name === `${CONST.TIMING.SIDEBAR_LOADED}_end`) {
+ measureFailSafe('contentAppeared_To_screenTTI', 'contentAppeared', mark.name);
+ measureTTI(mark.name);
+ }
+ });
+});
+
+function setCustomMarksObserverEnabled(enabled = false): void {
+ if (!enabled) {
+ customMarksObserver.disconnect();
+ return;
+ }
+
+ customMarksObserver.disconnect();
+ customMarksObserver.observe({type: 'mark', buffered: true});
+}
+
+function getPerformanceMetrics(): PerformanceEntry[] {
+ return [
+ ...performance.getEntriesByName('nativeLaunch'),
+ ...performance.getEntriesByName('nativeLaunchEnd_To_appCreationStart'),
+ ...performance.getEntriesByName('appCreation'),
+ ...performance.getEntriesByName('appCreationEnd_To_contentAppeared'),
+ ...performance.getEntriesByName('contentAppeared_To_screenTTI'),
+ ...performance.getEntriesByName('runJsBundle'),
+ ...performance.getEntriesByName('jsBundleDownload'),
+ ...performance.getEntriesByName('TTI'),
+ ...performance.getEntriesByName('regularAppStart'),
+ ...performance.getEntriesByName('appStartedToReady'),
+ ].filter((entry) => entry.duration > 0);
+}
+
+function getPerformanceMeasures(): PerformanceEntry[] {
+ return performance.getEntriesByType('measure');
+}
+
+/**
+ * Outputs performance stats. We alert these so that they are easy to access in release builds.
+ */
+function printPerformanceMetrics(): void {
+ const stats = getPerformanceMetrics();
+ const statsAsText = stats.map((entry) => `\u2022 ${entry.name}: ${entry.duration.toFixed(1)}ms`).join('\n');
+
+ if (stats.length > 0) {
+ Alert.alert('Performance', statsAsText);
+ }
+}
+
+function subscribeToMeasurements(callback: (entry: PerformanceEntry) => void): () => void {
+ const observer = new PerformanceObserver((list) => {
+ list.getEntriesByType('measure').forEach(callback);
+ });
+
+ observer.observe({type: 'measure', buffered: true});
+
+ return () => observer.disconnect();
+}
+
+/**
+ * Add a start mark to the performance entries
+ */
+function markStart(name: string, detail?: Record): PerformanceMark {
+ return performance.mark(`${name}_start`, {detail});
+}
+
+/**
+ * Add an end mark to the performance entries
+ * A measure between start and end is captured automatically
+ */
+function markEnd(name: string, detail?: Record): PerformanceMark {
+ return performance.mark(`${name}_end`, {detail});
+}
+
+type Phase = 'mount' | 'update' | 'nested-update';
+
+/**
+ * Put data emitted by Profiler components on the timeline
+ * @param id the "id" prop of the Profiler tree that has just committed
+ * @param phase either "mount" (if the tree just mounted) or "update" (if it re-rendered)
+ * @param actualDuration time spent rendering the committed update
+ * @param baseDuration estimated time to render the entire subtree without memoization
+ * @param startTime when React began rendering this update
+ * @param commitTime when React committed this update
+ * @param interactions the Set of interactions belonging to this update
+ */
+function traceRender(id: string, phase: Phase, actualDuration: number, baseDuration: number, startTime: number, commitTime: number, interactions: Set): PerformanceMeasure {
+ return performance.measure(id, {
+ start: startTime,
+ duration: actualDuration,
+ detail: {
+ phase,
+ baseDuration,
+ commitTime,
+ interactions,
+ },
+ });
+}
+
+type WrappedComponentConfig = {id: string};
+
+/**
+ * A HOC that captures render timings of the Wrapped component
+ */
+function withRenderTrace({id}: WrappedComponentConfig) {
+ if (!Metrics.canCapturePerformanceMetrics()) {
+ return >(WrappedComponent: React.ComponentType
): React.ComponentType
=> WrappedComponent;
+ }
+
+ return
>(WrappedComponent: React.ComponentType
): React.ComponentType
> => {
+ const WithRenderTrace: React.ComponentType
> = forwardRef((props: P, ref) => (
+
+
+
+ ));
+
+ WithRenderTrace.displayName = `withRenderTrace(${getComponentDisplayName(WrappedComponent as React.ComponentType)})`;
+ return WithRenderTrace;
};
+}
- /**
- * Add a start mark to the performance entries
- */
- Performance.markStart = (name: string, detail?: Record): PerformanceMark => rnPerformance.mark(`${name}_start`, {detail});
-
- /**
- * Add an end mark to the performance entries
- * A measure between start and end is captured automatically
- */
- Performance.markEnd = (name: string, detail?: Record): PerformanceMark => rnPerformance.mark(`${name}_end`, {detail});
-
- /**
- * Put data emitted by Profiler components on the timeline
- * @param id the "id" prop of the Profiler tree that has just committed
- * @param phase either "mount" (if the tree just mounted) or "update" (if it re-rendered)
- * @param actualDuration time spent rendering the committed update
- * @param baseDuration estimated time to render the entire subtree without memoization
- * @param startTime when React began rendering this update
- * @param commitTime when React committed this update
- * @param interactions the Set of interactions belonging to this update
- */
- Performance.traceRender = (
- id: string,
- phase: Phase,
- actualDuration: number,
- baseDuration: number,
- startTime: number,
- commitTime: number,
- interactions: Set,
- ): PerformanceMeasure =>
- rnPerformance.measure(id, {
- start: startTime,
- duration: actualDuration,
- detail: {
- phase,
- baseDuration,
- commitTime,
- interactions,
- },
- });
+function enableMonitoring() {
+ setResourceLoggingEnabled(true);
+ setNativeMarksObserverEnabled(true);
+ setCustomMarksObserverEnabled(true);
+}
- /**
- * A HOC that captures render timings of the Wrapped component
- */
- Performance.withRenderTrace =
- ({id}: WrappedComponentConfig) =>
- // eslint-disable-next-line @typescript-eslint/naming-convention
- >(WrappedComponent: React.ComponentType
): React.ComponentType
> => {
- const WithRenderTrace: React.ComponentType
> = forwardRef((props: P, ref) => (
-
-
-
- ));
-
- WithRenderTrace.displayName = `withRenderTrace(${getComponentDisplayName(WrappedComponent as React.ComponentType)})`;
- return WithRenderTrace;
- };
+function disableMonitoring() {
+ setResourceLoggingEnabled(false);
+ setNativeMarksObserverEnabled(false);
+ setCustomMarksObserverEnabled(false);
}
-export default Performance;
+export default {
+ diffObject,
+ measureFailSafe,
+ measureTTI,
+ enableMonitoring,
+ disableMonitoring,
+ getPerformanceMetrics,
+ getPerformanceMeasures,
+ printPerformanceMetrics,
+ subscribeToMeasurements,
+ markStart,
+ markEnd,
+ withRenderTrace,
+};
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index f9bf53bdd362..1ab0807c1900 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -42,6 +42,10 @@ function canUseLinkPreviews(): boolean {
return false;
}
+function canUseMergeAccounts(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.NEWDOT_MERGE_ACCOUNTS) || canUseAllBetas(betas);
+}
+
export default {
canUseDefaultRooms,
canUseLinkPreviews,
@@ -50,4 +54,5 @@ export default {
canUseCombinedTrackSubmit,
canUseCategoryAndTagApprovers,
canUsePerDiem,
+ canUseMergeAccounts,
};
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 91381a0b0119..2a4168a24668 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -257,7 +257,7 @@ const isPolicyEmployee = (policyID: string | undefined, policies: OnyxCollection
/**
* Checks if the current user is an owner (creator) of the policy.
*/
-const isPolicyOwner = (policy: OnyxInputOrEntry, currentUserAccountID: number): boolean => policy?.ownerAccountID === currentUserAccountID;
+const isPolicyOwner = (policy: OnyxInputOrEntry, currentUserAccountID: number | undefined): boolean => !!currentUserAccountID && policy?.ownerAccountID === currentUserAccountID;
/**
* Create an object mapping member emails to their accountIDs. Filter for members without errors if includeMemberWithErrors is false, and get the login email from the personalDetail object using the accountID.
@@ -420,7 +420,7 @@ function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry): PolicyEmployee[] {
/**
* Returns the policy of the report
*/
-function getPolicy(policyID: string | undefined): OnyxEntry {
- if (!allPolicies || !policyID) {
+function getPolicy(policyID: string | undefined, policies: OnyxCollection = allPolicies): OnyxEntry {
+ if (!policies || !policyID) {
return undefined;
}
- return allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
+ return policies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
}
/** Return active policies where current user is an admin */
@@ -676,6 +676,12 @@ function canSendInvoiceFromWorkspace(policyID: string | undefined): boolean {
return policy?.areInvoicesEnabled ?? false;
}
+/** Whether the user can submit per diem expense from the workspace */
+function canSubmitPerDiemExpenseFromWorkspace(policy: OnyxEntry): boolean {
+ const perDiemCustomUnit = getPerDiemCustomUnit(policy);
+ return !isEmptyObject(perDiemCustomUnit) && !!perDiemCustomUnit?.enabled;
+}
+
/** Whether the user can send invoice */
function canSendInvoice(policies: OnyxCollection | null, currentUserLogin: string | undefined): boolean {
return getActiveAdminWorkspaces(policies, currentUserLogin).some((policy) => canSendInvoiceFromWorkspace(policy.id));
@@ -1241,6 +1247,7 @@ export {
getActiveAdminWorkspaces,
getOwnedPaidPolicies,
canSendInvoiceFromWorkspace,
+ canSubmitPerDiemExpenseFromWorkspace,
canSendInvoice,
hasWorkspaceWithInvoices,
hasDependentTags,
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 4fa704944bc9..c1f4057199ee 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -286,7 +286,7 @@ function isWhisperActionTargetedToOthers(reportAction: OnyxInputOrEntry): reportAction is ReportAction {
@@ -347,7 +347,7 @@ function isInviteOrRemovedAction(
/**
* Returns whether the comment is a thread parent message/the first message in a thread
*/
-function isThreadParentMessage(reportAction: OnyxEntry, reportID: string): boolean {
+function isThreadParentMessage(reportAction: OnyxEntry, reportID: string | undefined): boolean {
const {childType, childVisibleActionCount = 0, childReportID} = reportAction ?? {};
return childType === CONST.REPORT.TYPE.CHAT && (childVisibleActionCount > 0 || String(childReportID) === reportID);
}
@@ -771,7 +771,11 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo
return updatedReportAction;
}
-function getLastVisibleAction(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}): OnyxEntry {
+function getLastVisibleAction(
+ reportID: string | undefined,
+ canUserPerformWriteAction?: boolean,
+ actionsToMerge: Record | null> = {},
+): OnyxEntry {
let reportActions: Array = [];
if (!isEmpty(actionsToMerge)) {
reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)) as Array<
@@ -801,7 +805,7 @@ function formatLastMessageText(lastMessageText: string) {
}
function getLastVisibleMessage(
- reportID: string,
+ reportID: string | undefined,
canUserPerformWriteAction?: boolean,
actionsToMerge: Record | null> = {},
reportAction: OnyxInputOrEntry | undefined = undefined,
@@ -893,12 +897,12 @@ function getLastClosedReportAction(reportActions: OnyxEntry): Ony
* 4. We will get the second last action from filtered actions because the last
* action is always the created action
*/
-function getFirstVisibleReportActionID(sortedReportActions: ReportAction[] = [], isOffline = false): string {
+function getFirstVisibleReportActionID(sortedReportActions: ReportAction[] = [], isOffline = false): string | undefined {
if (!Array.isArray(sortedReportActions)) {
return '';
}
const sortedFilterReportActions = sortedReportActions.filter((action) => !isDeletedAction(action) || (action?.childVisibleActionCount ?? 0) > 0 || isOffline);
- return sortedFilterReportActions.length > 1 ? sortedFilterReportActions.at(sortedFilterReportActions.length - 2)?.reportActionID ?? '-1' : '';
+ return sortedFilterReportActions.length > 1 ? sortedFilterReportActions.at(sortedFilterReportActions.length - 2)?.reportActionID : undefined;
}
/**
@@ -991,8 +995,8 @@ function getReportPreviewAction(chatReportID: string | undefined, iouReportID: s
/**
* Get the iouReportID for a given report action.
*/
-function getIOUReportIDFromReportActionPreview(reportAction: OnyxEntry): string {
- return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) ? getOriginalMessage(reportAction)?.linkedReportID ?? '-1' : '-1';
+function getIOUReportIDFromReportActionPreview(reportAction: OnyxEntry): string | undefined {
+ return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) ? getOriginalMessage(reportAction)?.linkedReportID : undefined;
}
/**
@@ -1057,7 +1061,11 @@ const iouRequestTypes = new Set>([
* Gets the reportID for the transaction thread associated with a report by iterating over the reportActions and identifying the IOU report actions.
* Returns a reportID if there is exactly one transaction thread for the report, and null otherwise.
*/
-function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEntry | ReportAction[], isOffline: boolean | undefined = undefined): string | undefined {
+function getOneTransactionThreadReportID(
+ reportID: string | undefined,
+ reportActions: OnyxEntry | ReportAction[],
+ isOffline: boolean | undefined = undefined,
+): string | undefined {
// If the report is not an IOU, Expense report, or Invoice, it shouldn't be treated as one-transaction report.
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (report?.type !== CONST.REPORT.TYPE.IOU && report?.type !== CONST.REPORT.TYPE.EXPENSE && report?.type !== CONST.REPORT.TYPE.INVOICE) {
@@ -1126,7 +1134,7 @@ function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteActio
return visibleReportActionsWithoutTaskSystemMessage.length > 0;
}
-function getAllReportActions(reportID: string): ReportActions {
+function getAllReportActions(reportID: string | undefined): ReportActions {
return allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {};
}
@@ -1500,7 +1508,7 @@ function isReportActionUnread(reportAction: OnyxEntry, lastReadTim
*/
function isCurrentActionUnread(report: OnyxEntry, reportAction: ReportAction): boolean {
const lastReadTime = report?.lastReadTime ?? '';
- const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(report?.reportID ?? '-1')));
+ const sortedReportActions = getSortedReportActions(Object.values(getAllReportActions(report?.reportID)));
const currentActionIndex = sortedReportActions.findIndex((action) => action.reportActionID === reportAction.reportActionID);
if (currentActionIndex === -1) {
return false;
@@ -1560,7 +1568,8 @@ function getDismissedViolationMessageText(originalMessage: ReportAction) {
@@ -1577,7 +1586,7 @@ function didMessageMentionCurrentUser(reportAction: OnyxInputOrEntry');
+ return accountIDsFromMessage.includes(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID) || emailsFromMessage.includes(currentEmail) || message.includes('');
}
/**
@@ -1592,9 +1601,9 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry {
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
- const reportActions = getAllReportActions(report?.reportID ?? '');
+ const reportActions = getAllReportActions(report?.reportID);
const action = Object.values(reportActions ?? {})?.find((reportAction) => {
- const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : -1;
+ const IOUTransactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : undefined;
return IOUTransactionID === transactionID;
});
return action;
@@ -1641,7 +1650,7 @@ function getExportIntegrationActionFragments(reportAction: OnyxEntry '2022-11-14';
const base62ReportID = getBase62ReportID(Number(reportID));
@@ -1775,7 +1784,7 @@ function getRenamedAction(reportAction: OnyxEntry>) {
const originalMessage = getOriginalMessage(reportAction);
- const submittersNames = PersonalDetailsUtils.getPersonalDetailsByIDs(originalMessage?.submittersAccountIDs ?? [], currentUserAccountID ?? -1).map(
+ const submittersNames = PersonalDetailsUtils.getPersonalDetailsByIDs(originalMessage?.submittersAccountIDs ?? [], currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID).map(
({displayName, login}) => displayName ?? login ?? 'Unknown Submitter',
);
return Localize.translateLocal('workspaceActions.removedFromApprovalWorkflow', {submittersNames, count: submittersNames.length});
@@ -1802,9 +1811,9 @@ function getCardIssuedMessage(reportAction: OnyxEntry, shouldRende
? getOriginalMessage(reportAction)
: undefined;
- const assigneeAccountID = cardIssuedActionOriginalMessage?.assigneeAccountID ?? -1;
- const cardID = cardIssuedActionOriginalMessage?.cardID ?? -1;
- const assigneeDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([assigneeAccountID], currentUserAccountID ?? -1).at(0);
+ const assigneeAccountID = cardIssuedActionOriginalMessage?.assigneeAccountID ?? CONST.DEFAULT_NUMBER_ID;
+ const cardID = cardIssuedActionOriginalMessage?.cardID ?? CONST.DEFAULT_NUMBER_ID;
+ const assigneeDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([assigneeAccountID], currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID).at(0);
const isPolicyAdmin = PolicyUtils.isPolicyAdmin(PolicyUtils.getPolicy(policyID));
const assignee = shouldRenderHTML ? `` : assigneeDetails?.firstName ?? assigneeDetails?.login ?? '';
const navigateRoute = isPolicyAdmin ? ROUTES.EXPENSIFY_CARD_DETAILS.getRoute(policyID, String(cardID)) : ROUTES.SETTINGS_DOMAINCARD_DETAIL.getRoute(String(cardID));
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 589c5f1a6adc..ff6b8d633447 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -1,7 +1,6 @@
import {format} from 'date-fns';
import {Str} from 'expensify-common';
import lodashEscape from 'lodash/escape';
-import lodashFindLastIndex from 'lodash/findLastIndex';
import lodashIntersection from 'lodash/intersection';
import isEmpty from 'lodash/isEmpty';
import lodashIsEqual from 'lodash/isEqual';
@@ -612,7 +611,9 @@ let currentUserPersonalDetails: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
- currentUserPersonalDetails = value?.[currentUserAccountID ?? -1] ?? undefined;
+ if (currentUserAccountID) {
+ currentUserPersonalDetails = value?.[currentUserAccountID] ?? undefined;
+ }
allPersonalDetails = value ?? {};
allPersonalDetailLogins = Object.values(allPersonalDetails).map((personalDetail) => personalDetail?.login ?? '');
},
@@ -787,6 +788,14 @@ function getReportOrDraftReport(reportID: string | undefined): OnyxEntry
return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? allReportsDraft?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT}${reportID}`];
}
+function getReportTransactions(reportID: string | undefined): Transaction[] {
+ if (!reportID) {
+ return [];
+ }
+
+ return reportsTransactions[reportID] ?? [];
+}
+
/**
* Check if a report is a draft report
*/
@@ -799,7 +808,7 @@ function isDraftReport(reportID: string | undefined): boolean {
/**
* Returns the report
*/
-function getReport(reportID: string): OnyxEntry {
+function getReport(reportID: string | undefined): OnyxEntry {
return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
}
@@ -807,7 +816,7 @@ function getReport(reportID: string): OnyxEntry {
* Returns the report
*/
function getReportNameValuePairs(reportID?: string): OnyxEntry {
- return allReportNameValuePair?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID ?? -1}`];
+ return allReportNameValuePair?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`];
}
/**
@@ -1051,8 +1060,8 @@ function isSettled(reportOrID: OnyxInputOrEntry | SearchReport | string
/**
* Whether the current user is the submitter of the report
*/
-function isCurrentUserSubmitter(reportID: string): boolean {
- if (!allReports) {
+function isCurrentUserSubmitter(reportID: string | undefined): boolean {
+ if (!allReports || !reportID) {
return false;
}
const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
@@ -1308,7 +1317,8 @@ function getDefaultNotificationPreferenceForReport(report: OnyxEntry): V
* Get the notification preference given a report. This should ALWAYS default to 'hidden'. Do not change this!
*/
function getReportNotificationPreference(report: OnyxEntry): ValueOf {
- return report?.participants?.[currentUserAccountID ?? -1]?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
+ const participant = currentUserAccountID ? report?.participants?.[currentUserAccountID] : undefined;
+ return participant?.notificationPreference ?? CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN;
}
const CONCIERGE_ACCOUNT_ID_STRING = CONST.ACCOUNT_ID.CONCIERGE.toString();
@@ -1643,7 +1653,11 @@ function isPolicyExpenseChatAdmin(report: OnyxEntry, policies: OnyxColle
/**
* Checks if the current user is the admin of the policy.
*/
-function isPolicyAdmin(policyID: string, policies: OnyxCollection): boolean {
+function isPolicyAdmin(policyID: string | undefined, policies: OnyxCollection): boolean {
+ if (!policyID) {
+ return false;
+ }
+
const policyRole = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.role;
return policyRole === CONST.POLICY.ROLE.ADMIN;
@@ -1653,7 +1667,7 @@ function isPolicyAdmin(policyID: string, policies: OnyxCollection): bool
* Checks whether all the transactions linked to the IOU report are of the Distance Request type with pending routes
*/
function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined): boolean {
- const transactions = reportsTransactions[iouReportID ?? ''] ?? [];
+ const transactions = getReportTransactions(iouReportID);
// Early return false in case not having any transaction
if (!transactions || transactions.length === 0) {
@@ -1737,11 +1751,7 @@ function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | SearchRepor
* Checks if a report contains only Non-Reimbursable transactions
*/
function hasOnlyNonReimbursableTransactions(iouReportID: string | undefined): boolean {
- if (!iouReportID) {
- return false;
- }
-
- const transactions = reportsTransactions[iouReportID ?? ''] ?? [];
+ const transactions = getReportTransactions(iouReportID);
if (!transactions || transactions.length === 0) {
return false;
}
@@ -1752,7 +1762,7 @@ function hasOnlyNonReimbursableTransactions(iouReportID: string | undefined): bo
/**
* Checks if a report has only one transaction associated with it
*/
-function isOneTransactionReport(reportID: string): boolean {
+function isOneTransactionReport(reportID: string | undefined): boolean {
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? ([] as ReportAction[]);
return ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions) !== null;
}
@@ -1786,9 +1796,9 @@ function isOneTransactionThread(reportID: string | undefined, parentReportID: st
*/
function getDisplayedReportID(reportID: string): string {
const report = getReport(reportID);
- const parentReportID = report?.parentReportID ?? '';
- const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, report?.parentReportActionID ?? '');
- return isOneTransactionThread(reportID, parentReportID, parentReportAction) ? parentReportID : reportID;
+ const parentReportID = report?.parentReportID;
+ const parentReportAction = ReportActionsUtils.getReportAction(parentReportID, report?.parentReportActionID);
+ return parentReportID && isOneTransactionThread(reportID, parentReportID, parentReportAction) ? parentReportID : reportID;
}
/**
@@ -1797,7 +1807,8 @@ function getDisplayedReportID(reportID: string): string {
*/
function isOneOnOneChat(report: OnyxEntry): boolean {
const participants = report?.participants ?? {};
- const isCurrentUserParticipant = participants[currentUserAccountID ?? 0] ? 1 : 0;
+ const participant = currentUserAccountID ? participants[currentUserAccountID] : undefined;
+ const isCurrentUserParticipant = participant ? 1 : 0;
const participantAmount = Object.keys(participants).length - isCurrentUserParticipant;
if (participantAmount !== 1) {
return false;
@@ -2381,6 +2392,8 @@ function getIcons(
policy?: OnyxInputOrEntry,
invoiceReceiverPolicy?: OnyxInputOrEntry,
): Icon[] {
+ const ownerDetails = report?.ownerAccountID ? personalDetails?.[report.ownerAccountID] : undefined;
+
if (isEmptyObject(report)) {
const fallbackIcon: Icon = {
source: defaultIcon ?? FallbackAvatar,
@@ -2393,12 +2406,13 @@ function getIcons(
if (isExpenseRequest(report)) {
const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
const workspaceIcon = getWorkspaceIcon(report, policy);
+ const actorDetails = parentReportAction?.actorAccountID ? personalDetails?.[parentReportAction.actorAccountID] : undefined;
const memberIcon = {
- source: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: actorDetails?.avatar ?? FallbackAvatar,
id: parentReportAction?.actorAccountID,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.fallbackIcon,
+ name: actorDetails?.displayName ?? '',
+ fallbackIcon: actorDetails?.fallbackIcon,
};
return [memberIcon, workspaceIcon];
@@ -2407,13 +2421,14 @@ function getIcons(
const parentReportAction = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
const actorAccountID = getReportActionActorAccountID(parentReportAction, report, report);
- const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1], '', false);
+ const actorDetails = actorAccountID ? personalDetails?.[actorAccountID] : undefined;
+ const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(actorDetails, '', false);
const actorIcon = {
id: actorAccountID,
- source: personalDetails?.[actorAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: actorDetails?.avatar ?? FallbackAvatar,
name: LocalePhoneNumber.formatPhoneNumber(actorDisplayName),
type: CONST.ICON_TYPE_AVATAR,
- fallbackIcon: personalDetails?.[parentReportAction?.actorAccountID ?? -1]?.fallbackIcon,
+ fallbackIcon: actorDetails?.fallbackIcon,
};
if (isWorkspaceThread(report)) {
@@ -2425,10 +2440,10 @@ function getIcons(
if (isTaskReport(report)) {
const ownerIcon = {
id: report?.ownerAccountID,
- source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: ownerDetails?.avatar ?? FallbackAvatar,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon,
+ name: ownerDetails?.displayName ?? '',
+ fallbackIcon: ownerDetails?.fallbackIcon,
};
if (isWorkspaceTaskReport(report)) {
@@ -2475,33 +2490,34 @@ function getIcons(
if (isPolicyExpenseChat(report) || isExpenseReport(report)) {
const workspaceIcon = getWorkspaceIcon(report, policy);
const memberIcon = {
- source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: ownerDetails?.avatar ?? FallbackAvatar,
id: report?.ownerAccountID,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon,
+ name: ownerDetails?.displayName ?? '',
+ fallbackIcon: ownerDetails?.fallbackIcon,
};
return isExpenseReport(report) ? [memberIcon, workspaceIcon] : [workspaceIcon, memberIcon];
}
if (isIOUReport(report)) {
+ const managerDetails = report?.managerID ? personalDetails?.[report.managerID] : undefined;
const managerIcon = {
- source: personalDetails?.[report?.managerID ?? -1]?.avatar ?? FallbackAvatar,
+ source: managerDetails?.avatar ?? FallbackAvatar,
id: report?.managerID,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[report?.managerID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[report?.managerID ?? -1]?.fallbackIcon,
+ name: managerDetails?.displayName ?? '',
+ fallbackIcon: managerDetails?.fallbackIcon,
};
const ownerIcon = {
id: report?.ownerAccountID,
- source: personalDetails?.[report?.ownerAccountID ?? -1]?.avatar ?? FallbackAvatar,
+ source: ownerDetails?.avatar ?? FallbackAvatar,
type: CONST.ICON_TYPE_AVATAR,
- name: personalDetails?.[report?.ownerAccountID ?? -1]?.displayName ?? '',
- fallbackIcon: personalDetails?.[report?.ownerAccountID ?? -1]?.fallbackIcon,
+ name: ownerDetails?.displayName ?? '',
+ fallbackIcon: ownerDetails?.fallbackIcon,
};
const isManager = currentUserAccountID === report?.managerID;
// For one transaction IOUs, display a simplified report icon
- if (isOneTransactionReport(report?.reportID ?? '-1')) {
+ if (isOneTransactionReport(report?.reportID)) {
return [ownerIcon];
}
@@ -2509,7 +2525,7 @@ function getIcons(
}
if (isSelfDM(report)) {
- return getIconsForParticipants([currentUserAccountID ?? -1], personalDetails);
+ return getIconsForParticipants(currentUserAccountID ? [currentUserAccountID] : [], personalDetails);
}
if (isSystemChat(report)) {
@@ -2767,7 +2783,7 @@ function buildOptimisticCancelPaymentReportAction(expenseReportID: string, amoun
*/
function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: ReportActions = {}): LastVisibleMessage {
const report = getReportOrDraftReport(reportID);
- const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '-1', canUserPerformWriteAction(report), actionsToMerge);
+ const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction(report), actionsToMerge);
// For Chat Report with deleted parent actions, let us fetch the correct message
if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && !isEmptyObject(report) && isChatReport(report)) {
@@ -2778,7 +2794,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep
}
// Fetch the last visible message for report represented by reportID and based on actions to merge.
- return ReportActionsUtils.getLastVisibleMessage(reportID ?? '-1', canUserPerformWriteAction(report), actionsToMerge);
+ return ReportActionsUtils.getLastVisibleMessage(reportID, canUserPerformWriteAction(report), actionsToMerge);
}
/**
@@ -2906,11 +2922,10 @@ function requiresAttentionFromCurrentUser(optionOrReport: OnyxEntry | Op
}
/**
- * Returns number of transactions that are nonReimbursable
- *
+ * Checks if the report contains at least one Non-Reimbursable transaction
*/
function hasNonReimbursableTransactions(iouReportID: string | undefined): boolean {
- const transactions = reportsTransactions[iouReportID ?? ''] ?? [];
+ const transactions = getReportTransactions(iouReportID);
return transactions.filter((transaction) => transaction.reimbursable === false).length > 0;
}
@@ -2956,7 +2971,7 @@ function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, allRepo
*/
function getPolicyExpenseChatName(report: OnyxEntry, policy?: OnyxEntry): string | undefined {
const ownerAccountID = report?.ownerAccountID;
- const personalDetails = allPersonalDetails?.[ownerAccountID ?? -1];
+ const personalDetails = ownerAccountID ? allPersonalDetails?.[ownerAccountID] : undefined;
const login = personalDetails ? personalDetails.login : null;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const reportOwnerDisplayName = getDisplayNameForParticipant(ownerAccountID) || login || report?.reportName;
@@ -2975,7 +2990,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy?: OnyxEntry<
// If this user is not admin and this policy expense chat has been archived because of account merging, this must be an old workspace chat
// of the account which was merged into the current user's account. Use the name of the policy as the name of the report.
if (isArchivedRoom(report, getReportNameValuePairs(report?.reportID))) {
- const lastAction = ReportActionsUtils.getLastVisibleAction(report?.reportID ?? '-1');
+ const lastAction = ReportActionsUtils.getLastVisibleAction(report?.reportID);
const archiveReason = ReportActionsUtils.isClosedAction(lastAction) ? ReportActionsUtils.getOriginalMessage(lastAction)?.reason : CONST.REPORT.ARCHIVE_REASON.DEFAULT;
if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED && policyExpenseChatRole !== CONST.POLICY.ROLE.ADMIN) {
return getPolicyName(report, false, policy);
@@ -3006,7 +3021,7 @@ function isReportFieldOfTypeTitle(reportField: OnyxEntry): bo
/**
* Check if Report has any held expenses
*/
-function isHoldCreator(transaction: OnyxEntry, reportID: string): boolean {
+function isHoldCreator(transaction: OnyxEntry, reportID: string | undefined): boolean {
const holdReportAction = ReportActionsUtils.getReportAction(reportID, `${transaction?.comment?.hold ?? ''}`);
return isActionCreator(holdReportAction);
}
@@ -3022,7 +3037,7 @@ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry
const isReportSettled = isSettled(report?.reportID);
const isReportClosed = isClosedReport(report);
const isTitleField = isReportFieldOfTypeTitle(reportField);
- const isAdmin = isPolicyAdmin(report?.policyID ?? '-1', {[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id ?? '-1'}`]: policy});
+ const isAdmin = isPolicyAdmin(report?.policyID, {[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`]: policy});
return isTitleField ? !reportField?.deletable : !isAdmin && (isReportSettled || isReportClosed);
}
@@ -3053,7 +3068,7 @@ function getReportFieldKey(reportFieldId: string | undefined) {
/**
* Get the report fields attached to the policy given policyID
*/
-function getReportFieldsByPolicyID(policyID: string): Record {
+function getReportFieldsByPolicyID(policyID: string | undefined): Record {
const policyReportFields = Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID);
const fieldList = policyReportFields?.[1]?.fieldList;
@@ -3105,8 +3120,8 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo
* Get the title for an IOU or expense chat which will be showing the payer and the amount
*/
function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry, invoiceReceiverPolicy?: OnyxEntry): string {
- const isReportSettled = isSettled(report?.reportID ?? '-1');
- const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID ?? '-1');
+ const isReportSettled = isSettled(report?.reportID);
+ const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID);
const titleReportField = Object.values(reportFields ?? {}).find((reportField) => reportField?.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID);
if (titleReportField && report?.reportName && isPaidGroupPolicyExpenseReport(report)) {
@@ -3229,7 +3244,7 @@ function canEditMoneyRequest(reportAction: OnyxInputOrEntry)
return {canHoldRequest: false, canUnholdRequest: false};
}
- const moneyRequestReportID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID ?? 0;
+ const moneyRequestReportID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID;
const moneyRequestReport = getReportOrDraftReport(String(moneyRequestReportID));
if (!moneyRequestReportID || !moneyRequestReport) {
@@ -3375,7 +3390,7 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry)
const isRequestSettled = isSettled(moneyRequestReport?.reportID);
const isApproved = isReportApproved(moneyRequestReport);
- const transactionID = moneyRequestReport ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID : 0;
+ const transactionID = moneyRequestReport ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID : undefined;
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? ({} as Transaction);
const parentReportAction = isThread(moneyRequestReport)
@@ -3383,7 +3398,7 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry)
: undefined;
const isRequestIOU = isIOUReport(moneyRequestReport);
- const isHoldActionCreator = isHoldCreator(transaction, reportAction.childReportID ?? '-1');
+ const isHoldActionCreator = isHoldCreator(transaction, reportAction.childReportID);
const isTrackExpenseMoneyReport = isTrackExpenseReport(moneyRequestReport);
const isActionOwner =
@@ -3391,7 +3406,7 @@ function canHoldUnholdReportAction(reportAction: OnyxInputOrEntry)
typeof currentUserPersonalDetails?.accountID === 'number' &&
parentReportAction.actorAccountID === currentUserPersonalDetails?.accountID;
const isApprover = isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && currentUserPersonalDetails?.accountID === moneyRequestReport?.managerID;
- const isAdmin = isPolicyAdmin(moneyRequestReport.policyID ?? '-1', allPolicies);
+ const isAdmin = isPolicyAdmin(moneyRequestReport.policyID, allPolicies);
const isOnHold = TransactionUtils.isOnHold(transaction);
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
const isClosed = isClosedReport(moneyRequestReport);
@@ -3413,25 +3428,31 @@ const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, bac
if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) {
return;
}
- const moneyRequestReportID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID ?? 0;
+ const moneyRequestReportID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID;
const moneyRequestReport = getReportOrDraftReport(String(moneyRequestReportID));
if (!moneyRequestReportID || !moneyRequestReport) {
return;
}
- const transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID ?? '';
+ const transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID;
+
+ if (!transactionID || !reportAction.childReportID) {
+ Log.warn('Missing transactionID and reportAction.childReportID during the change of the money request hold status');
+ return;
+ }
+
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? ({} as Transaction);
const isOnHold = TransactionUtils.isOnHold(transaction);
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport.policyID}`] ?? null;
if (isOnHold) {
- IOU.unholdRequest(transactionID, reportAction.childReportID ?? '', searchHash);
+ IOU.unholdRequest(transactionID, reportAction.childReportID, searchHash);
} else {
const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams());
Navigation.navigate(
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, reportAction.childReportID ?? '', backTo || activeRoute, searchHash),
+ ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, reportAction.childReportID, backTo || activeRoute, searchHash),
);
}
};
@@ -3440,7 +3461,7 @@ const changeMoneyRequestHoldStatus = (reportAction: OnyxEntry, bac
* Gets all transactions on an IOU report with a receipt
*/
function getTransactionsWithReceipts(iouReportID: string | undefined): Transaction[] {
- const transactions = reportsTransactions[iouReportID ?? ''] ?? [];
+ const transactions = getReportTransactions(iouReportID);
return transactions.filter((transaction) => TransactionUtils.hasReceipt(transaction));
}
@@ -3467,10 +3488,10 @@ function areAllRequestsBeingSmartScanned(iouReportID: string, reportPreviewActio
* NOTE: This method is only meant to be used inside this action file. Do not export and use it elsewhere. Use withOnyx or Onyx.connect() instead.
*/
function getLinkedTransaction(reportAction: OnyxEntry): OnyxEntry {
- let transactionID = '';
+ let transactionID;
if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
- transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID ?? '-1';
+ transactionID = ReportActionsUtils.getOriginalMessage(reportAction)?.IOUTransactionID;
}
return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
@@ -3480,7 +3501,7 @@ function getLinkedTransaction(reportAction: OnyxEntry {
if (!ReportActionsUtils.isMoneyRequestAction(action)) {
@@ -3508,7 +3529,7 @@ function getReportActionWithMissingSmartscanFields(iouReportID: string): ReportA
/**
* Check if iouReportID has required missing fields
*/
-function shouldShowRBRForMissingSmartscanFields(iouReportID: string): boolean {
+function shouldShowRBRForMissingSmartscanFields(iouReportID: string | undefined): boolean {
return !!getReportActionWithMissingSmartscanFields(iouReportID);
}
@@ -3683,7 +3704,7 @@ function getReportPreviewMessage(
}
if (report.isWaitingOnBankAccount) {
- const submitterDisplayName = getDisplayNameForParticipant(report.ownerAccountID ?? -1, true) ?? '';
+ const submitterDisplayName = getDisplayNameForParticipant(report.ownerAccountID, true) ?? '';
return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName});
}
@@ -3832,15 +3853,15 @@ function getAdminRoomInvitedParticipants(parentReportAction: OnyxEntry {
- const name = getDisplayNameForParticipant(id);
+ const participants = personalDetails.map((personalDetail) => {
+ const name = PersonalDetailsUtils.getEffectiveDisplayName(personalDetail);
if (name && name?.length > 0) {
return name;
}
@@ -3883,7 +3904,7 @@ function getInvoicePayerName(report: OnyxEntry, invoiceReceiverPolicy?:
/**
* Parse html of reportAction into text
*/
-function parseReportActionHtmlToText(reportAction: OnyxEntry, reportID: string, childReportID?: string): string {
+function parseReportActionHtmlToText(reportAction: OnyxEntry, reportID: string | undefined, childReportID?: string): string {
if (!reportAction) {
return '';
}
@@ -3947,7 +3968,7 @@ function getReportActionMessage(reportAction: OnyxEntry, reportID?
return getReimbursementQueuedActionMessage(reportAction, getReportOrDraftReport(reportID), false);
}
- return parseReportActionHtmlToText(reportAction, reportID ?? '', childReportID);
+ return parseReportActionHtmlToText(reportAction, reportID, childReportID);
}
/**
@@ -3956,8 +3977,8 @@ function getReportActionMessage(reportAction: OnyxEntry, reportID?
function getInvoicesChatName(report: OnyxEntry, receiverPolicy: OnyxEntry): string {
const invoiceReceiver = report?.invoiceReceiver;
const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL;
- const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1;
- const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? '-1';
+ const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : CONST.DEFAULT_NUMBER_ID;
+ const invoiceReceiverPolicyID = isIndividual ? undefined : invoiceReceiver?.policyID;
const invoiceReceiverPolicy = receiverPolicy ?? getPolicy(invoiceReceiverPolicyID);
const isCurrentUserReceiver = (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyAdmin(invoiceReceiverPolicy));
@@ -4058,7 +4079,7 @@ function getReportName(
}
const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : undefined);
- const reportActionMessage = getReportActionMessage(parentReportAction, report?.parentReportID, report?.reportID ?? '').replace(/(\n+|\r\n|\n|\r)/gm, ' ');
+ const reportActionMessage = getReportActionMessage(parentReportAction, report?.parentReportID, report?.reportID).replace(/(\n+|\r\n|\n|\r)/gm, ' ');
if (isAttachment && reportActionMessage) {
return `[${Localize.translateLocal('common.attachment')}]`;
}
@@ -4271,7 +4292,11 @@ function goBackToDetailsPage(report: OnyxEntry, backTo?: string) {
return;
}
- Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report?.reportID ?? '-1', backTo));
+ if (report?.reportID) {
+ Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID, backTo));
+ } else {
+ Log.warn('Missing reportID during navigation back to the details page');
+ }
}
function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean) {
@@ -4458,7 +4483,7 @@ function buildOptimisticAddCommentReportAction(
const isAttachmentOnly = file && !text;
const isAttachmentWithText = !!text && file !== undefined;
- const accountID = actorAccountID ?? currentUserAccountID ?? -1;
+ const accountID = actorAccountID ?? currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID;
const delegateAccountDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
// Remove HTML from text when applying optimistic offline comment
@@ -4594,6 +4619,31 @@ function buildOptimisticTaskCommentReportAction(
return reportAction;
}
+function buildOptimisticSelfDMReport(created: string): Report {
+ return {
+ reportID: generateReportID(),
+ participants: {
+ [currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID]: {
+ notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE,
+ },
+ },
+ type: CONST.REPORT.TYPE.CHAT,
+ chatType: CONST.REPORT.CHAT_TYPE.SELF_DM,
+ isOwnPolicyExpenseChat: false,
+ isPinned: true,
+ lastActorAccountID: 0,
+ lastMessageHtml: '',
+ lastMessageText: undefined,
+ lastReadTime: created,
+ lastVisibleActionCreated: created,
+ ownerAccountID: currentUserAccountID,
+ reportName: '',
+ stateNum: 0,
+ statusNum: 0,
+ writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ALL,
+ };
+}
+
/**
* Builds an optimistic IOU report with a randomly generated reportID
*
@@ -4618,7 +4668,7 @@ function buildOptimisticIOUReport(
const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency);
const personalDetails = getPersonalDetailsForAccountID(payerAccountID);
const payerEmail = 'login' in personalDetails ? personalDetails.login : '';
- const policyID = getReport(chatReportID)?.policyID ?? '-1';
+ const policyID = getReport(chatReportID)?.policyID;
const policy = getPolicy(policyID);
const participants: Participants = {
@@ -4684,8 +4734,7 @@ function populateOptimisticReportFormula(formula: string, report: OptimisticExpe
/** Builds an optimistic invoice report with a randomly generated reportID */
function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, receiverAccountID: number, receiverName: string, total: number, currency: string): OptimisticExpenseReport {
const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency);
-
- return {
+ const invoiceReport = {
reportID: generateReportID(),
chatReportID,
policyID,
@@ -4699,9 +4748,6 @@ function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, re
statusNum: CONST.REPORT.STATUS_NUM.OPEN,
total,
participants: {
- [currentUserAccountID ?? -1]: {
- notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
- },
[receiverAccountID]: {
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
},
@@ -4709,6 +4755,12 @@ function buildOptimisticInvoiceReport(chatReportID: string, policyID: string, re
parentReportID: chatReportID,
lastVisibleActionCreated: DateUtils.getDBTime(),
};
+
+ if (currentUserAccountID) {
+ invoiceReport.participants[currentUserAccountID] = {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN};
+ }
+
+ return invoiceReport;
}
/**
@@ -5035,8 +5087,8 @@ function buildOptimisticIOUReportAction(
originalMessage.participantAccountIDs = currentUserAccountID ? [currentUserAccountID] : [];
} else {
originalMessage.participantAccountIDs = currentUserAccountID
- ? [currentUserAccountID, ...participants.map((participant) => participant.accountID ?? -1)]
- : participants.map((participant) => participant.accountID ?? -1);
+ ? [currentUserAccountID, ...participants.map((participant) => participant.accountID ?? CONST.DEFAULT_NUMBER_ID)]
+ : participants.map((participant) => participant.accountID ?? CONST.DEFAULT_NUMBER_ID);
}
}
@@ -5247,14 +5299,14 @@ function buildOptimisticReportPreview(
},
],
created,
- accountID: iouReport?.managerID ?? -1,
+ accountID: iouReport?.managerID,
// The preview is initially whispered if created with a receipt, so the actor is the current user as well
actorAccountID: hasReceipt ? currentUserAccountID : reportActorAccountID,
childReportID: childReportID ?? iouReport?.reportID,
childMoneyRequestCount: 1,
childLastActorAccountID: currentUserAccountID,
childLastMoneyRequestComment: comment,
- childRecentReceiptTransactionIDs: hasReceipt && !isEmptyObject(transaction) ? {[transaction?.transactionID ?? '-1']: created} : undefined,
+ childRecentReceiptTransactionIDs: hasReceipt && !isEmptyObject(transaction) && transaction?.transactionID ? {[transaction.transactionID]: created} : undefined,
};
}
@@ -5432,11 +5484,13 @@ function updateReportPreview(
: recentReceiptTransactions,
// As soon as we add a transaction without a receipt to the report, it will have ready expenses,
// so we remove the whisper
- originalMessage: {
- ...(originalMessage ?? {}),
- whisperedTo: hasReceipt ? originalMessage?.whisperedTo : [],
- linkedReportID: originalMessage?.linkedReportID ?? '0',
- },
+ originalMessage: originalMessage
+ ? {
+ ...originalMessage,
+ whisperedTo: hasReceipt ? originalMessage.whisperedTo : [],
+ linkedReportID: originalMessage.linkedReportID,
+ }
+ : undefined,
};
}
@@ -6086,7 +6140,7 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string, exp
const pendingChatMembers = getPendingChatMembers(currentUserAccountID ? [currentUserAccountID] : [], [], CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD);
const adminsChatData = {
...buildOptimisticChatReport(
- [currentUserAccountID ?? -1],
+ currentUserAccountID ? [currentUserAccountID] : [],
CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS,
CONST.REPORT.CHAT_TYPE.POLICY_ADMINS,
policyID,
@@ -6102,7 +6156,7 @@ function buildOptimisticWorkspaceChats(policyID: string, policyName: string, exp
};
const expenseChatData = buildOptimisticChatReport(
- [currentUserAccountID ?? -1],
+ currentUserAccountID ? [currentUserAccountID] : [],
'',
CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
policyID,
@@ -6246,7 +6300,7 @@ function buildTransactionThread(
participantAccountIDs,
getTransactionReportName(reportAction),
undefined,
- moneyRequestReport?.policyID ?? '-1',
+ moneyRequestReport?.policyID,
CONST.POLICY.OWNER_ACCOUNT_ID_FAKE,
false,
'',
@@ -6409,7 +6463,7 @@ function isReportNotFound(report: OnyxEntry): boolean {
/**
* Check if the report is the parent report of the currently viewed report or at least one child report has report action
*/
-function shouldHideReport(report: OnyxEntry, currentReportId: string): boolean {
+function shouldHideReport(report: OnyxEntry, currentReportId: string | undefined): boolean {
const currentReport = getReportOrDraftReport(currentReportId);
const parentReport = getParentReport(!isEmptyObject(currentReport) ? currentReport : undefined);
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? {};
@@ -6427,7 +6481,7 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV
}
// We only show the RBR to the submitter
- if (!isCurrentUserSubmitter(report.reportID ?? '')) {
+ if (!isCurrentUserSubmitter(report.reportID)) {
return false;
}
@@ -6447,7 +6501,7 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV
* Checks to see if a report contains a violation
*/
function hasViolations(reportID: string, transactionViolations: OnyxCollection, shouldShowInReview?: boolean): boolean {
- const transactions = reportsTransactions[reportID] ?? [];
+ const transactions = getReportTransactions(reportID);
return transactions.some((transaction) => TransactionUtils.hasViolation(transaction.transactionID, transactionViolations, shouldShowInReview));
}
@@ -6455,7 +6509,7 @@ function hasViolations(reportID: string, transactionViolations: OnyxCollection, shouldShowInReview?: boolean): boolean {
- const transactions = reportsTransactions[reportID] ?? [];
+ const transactions = getReportTransactions(reportID);
return transactions.some((transaction) => TransactionUtils.hasWarningTypeViolation(transaction.transactionID, transactionViolations, shouldShowInReview));
}
@@ -6463,7 +6517,7 @@ function hasWarningTypeViolations(reportID: string, transactionViolations: OnyxC
* Checks to see if a report contains a violation of type `notice`
*/
function hasNoticeTypeViolations(reportID: string, transactionViolations: OnyxCollection, shouldShowInReview?: boolean): boolean {
- const transactions = reportsTransactions[reportID] ?? [];
+ const transactions = getReportTransactions(reportID);
return transactions.some((transaction) => TransactionUtils.hasNoticeTypeViolation(transaction.transactionID, transactionViolations, shouldShowInReview));
}
@@ -6479,7 +6533,7 @@ function hasReportViolations(reportID: string) {
function shouldAdminsRoomBeVisible(report: OnyxEntry): boolean {
const accountIDs = Object.entries(report?.participants ?? {}).map(([accountID]) => Number(accountID));
const adminAccounts = PersonalDetailsUtils.getLoginsByAccountIDs(accountIDs).filter((login) => !PolicyUtils.isExpensifyTeam(login));
- const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(report?.reportID ?? '');
+ const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(report?.reportID);
if ((lastVisibleAction ? ReportActionsUtils.isCreatedAction(lastVisibleAction) : report?.lastActionType === CONST.REPORT.ACTIONS.TYPE.CREATED) && adminAccounts.length <= 1) {
return false;
}
@@ -6508,7 +6562,7 @@ function getAllReportActionsErrorsAndReportActionThatRequiresAttention(report: O
const parentReportAction: OnyxEntry =
!report?.parentReportID || !report?.parentReportActionID
? undefined
- : allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID ?? '-1'}`]?.[report.parentReportActionID ?? '-1'];
+ : allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`]?.[report.parentReportActionID];
if (!isArchivedRoom(report)) {
if (ReportActionsUtils.wasActionTakenByCurrentUser(parentReportAction) && ReportActionsUtils.isTransactionThread(parentReportAction)) {
@@ -6519,9 +6573,9 @@ function getAllReportActionsErrorsAndReportActionThatRequiresAttention(report: O
reportAction = undefined;
}
} else if ((isIOUReport(report) || isExpenseReport(report)) && report?.ownerAccountID === currentUserAccountID) {
- if (shouldShowRBRForMissingSmartscanFields(report?.reportID ?? '-1') && !isSettled(report?.reportID)) {
+ if (shouldShowRBRForMissingSmartscanFields(report?.reportID) && !isSettled(report?.reportID)) {
reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage');
- reportAction = getReportActionWithMissingSmartscanFields(report?.reportID ?? '-1');
+ reportAction = getReportActionWithMissingSmartscanFields(report?.reportID);
}
} else if (hasSmartscanError(reportActionsArray)) {
reportActionErrors.smartscan = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericSmartscanFailureMessage');
@@ -6579,7 +6633,7 @@ function hasReportErrorsOtherThanFailedReceipt(report: Report, doesReportHaveVio
type ShouldReportBeInOptionListParams = {
report: OnyxEntry;
- currentReportId: string;
+ currentReportId: string | undefined;
isInFocusMode: boolean;
betas: OnyxEntry;
policies: OnyxCollection;
@@ -6641,7 +6695,7 @@ function reasonForReportToBeInOptionList({
}
// If this is a transaction thread associated with a report that only has one transaction, omit it
- if (isOneTransactionThread(report.reportID, report.parentReportID ?? '-1', parentReportAction)) {
+ if (isOneTransactionThread(report.reportID, report.parentReportID, parentReportAction)) {
return null;
}
@@ -6724,11 +6778,7 @@ function reasonForReportToBeInOptionList({
}
// Hide chat threads where the parent message is pending removal
- if (
- !isEmptyObject(parentReportAction) &&
- ReportActionsUtils.isPendingRemove(parentReportAction) &&
- ReportActionsUtils.isThreadParentMessage(parentReportAction, report?.reportID ?? '')
- ) {
+ if (!isEmptyObject(parentReportAction) && ReportActionsUtils.isPendingRemove(parentReportAction) && ReportActionsUtils.isThreadParentMessage(parentReportAction, report?.reportID)) {
return null;
}
@@ -6793,10 +6843,11 @@ function getInvoiceChatByParticipants(receiverID: string | number, receiverType:
/**
* Attempts to find a policy expense report in onyx that is owned by ownerAccountID in a given policy
*/
-function getPolicyExpenseChat(ownerAccountID: number, policyID: string | undefined): OnyxEntry {
- if (!policyID) {
+function getPolicyExpenseChat(ownerAccountID: number | undefined, policyID: string | undefined): OnyxEntry {
+ if (!ownerAccountID || !policyID) {
return;
}
+
return Object.values(allReports ?? {}).find((report: OnyxEntry) => {
// If the report has been deleted, then skip it
if (!report) {
@@ -6878,21 +6929,6 @@ function shouldShowFlagComment(reportAction: OnyxInputOrEntry, rep
);
}
-/**
- * @param sortedAndFilteredReportActions - reportActions for the report, sorted newest to oldest, and filtered for only those that should be visible
- */
-function getNewMarkerReportActionID(report: OnyxEntry, sortedAndFilteredReportActions: ReportAction[]): string {
- if (!isUnread(report)) {
- return '';
- }
-
- const newMarkerIndex = lodashFindLastIndex(sortedAndFilteredReportActions, (reportAction) => (reportAction.created ?? '') > (report?.lastReadTime ?? ''));
-
- return newMarkerIndex !== -1 && 'reportActionID' in (sortedAndFilteredReportActions?.at(newMarkerIndex) ?? {})
- ? sortedAndFilteredReportActions.at(newMarkerIndex)?.reportActionID ?? ''
- : '';
-}
-
/**
* Performs the markdown conversion, and replaces code points > 127 with C escape sequences
* Used for compatibility with the backend auth validator for AddComment, and to account for MD in comments
@@ -7076,7 +7112,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, policy: OnyxEntry 0;
- const isPolicyOwnedByExpensifyAccounts = report?.policyID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(getPolicy(report?.policyID ?? '-1')?.ownerAccountID ?? -1) : false;
+ const policyOwnerAccountID = getPolicy(report?.policyID)?.ownerAccountID;
+ const isPolicyOwnedByExpensifyAccounts = policyOwnerAccountID ? CONST.EXPENSIFY_ACCOUNT_IDS.includes(policyOwnerAccountID) : false;
if (doParticipantsIncludeExpensifyAccounts && !isPolicyOwnedByExpensifyAccounts) {
return [];
}
@@ -7198,7 +7235,7 @@ function canLeaveRoom(report: OnyxEntry, isPolicyEmployee: boolean): boo
return false;
}
- const invoiceReport = getReportOrDraftReport(report?.iouReportID ?? '-1');
+ const invoiceReport = getReportOrDraftReport(report?.iouReportID);
if (invoiceReport?.ownerAccountID === currentUserAccountID) {
return false;
@@ -7281,7 +7318,7 @@ function shouldReportShowSubscript(report: OnyxEntry): boolean {
return true;
}
- if (isExpenseReport(report) && isOneTransactionReport(report?.reportID ?? '-1')) {
+ if (isExpenseReport(report) && isOneTransactionReport(report?.reportID)) {
return true;
}
@@ -7332,7 +7369,7 @@ function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry | str
return false;
}
- const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1');
+ const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID, report?.parentReportActionID);
return parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
}
@@ -7353,7 +7390,7 @@ function canUserPerformWriteAction(report: OnyxEntry) {
*/
function getOriginalReportID(reportID: string, reportAction: OnyxInputOrEntry): string | undefined {
const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
- const currentReportAction = reportActions?.[reportAction?.reportActionID ?? '-1'] ?? null;
+ const currentReportAction = reportAction?.reportActionID ? reportActions?.[reportAction.reportActionID] : undefined;
const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID, reportActions ?? ([] as ReportAction[]));
const isThreadReportParentAction = reportAction?.childReportID?.toString() === reportID;
if (Object.keys(currentReportAction ?? {}).length === 0) {
@@ -7398,7 +7435,9 @@ function canCreateRequest(report: OnyxEntry, policy: OnyxEntry,
}
function getWorkspaceChats(policyID: string, accountIDs: number[], reports: OnyxCollection = allReports): Array> {
- return Object.values(reports ?? {}).filter((report) => isPolicyExpenseChat(report) && (report?.policyID ?? '-1') === policyID && accountIDs.includes(report?.ownerAccountID ?? -1));
+ return Object.values(reports ?? {}).filter(
+ (report) => isPolicyExpenseChat(report) && report?.policyID === policyID && report?.ownerAccountID && accountIDs.includes(report?.ownerAccountID),
+ );
}
/**
@@ -7407,7 +7446,7 @@ function getWorkspaceChats(policyID: string, accountIDs: number[], reports: Onyx
* @param policyID - the workspace ID to get all associated reports
*/
function getAllWorkspaceReports(policyID: string): Array> {
- return Object.values(allReports ?? {}).filter((report) => (report?.policyID ?? '-1') === policyID);
+ return Object.values(allReports ?? {}).filter((report) => report?.policyID === policyID);
}
/**
@@ -7557,7 +7596,7 @@ function getTaskAssigneeChatOnyxData(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`,
- value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '-1']: optimisticAssigneeAddComment.reportAction as ReportAction},
+ value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: optimisticAssigneeAddComment.reportAction as ReportAction},
},
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -7568,12 +7607,12 @@ function getTaskAssigneeChatOnyxData(
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`,
- value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '-1']: {isOptimisticAction: null}},
+ value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: {isOptimisticAction: null}},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${assigneeChatReportID}`,
- value: {[optimisticAssigneeAddComment.reportAction.reportActionID ?? '-1']: {pendingAction: null}},
+ value: {[optimisticAssigneeAddComment.reportAction.reportActionID]: {pendingAction: null}},
});
}
@@ -7606,7 +7645,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry,
switch (originalMessage.paymentType) {
case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
- translationKey = hasMissingInvoiceBankAccount(IOUReportID ?? '-1') ? 'iou.payerSettledWithMissingBankAccount' : 'iou.paidElsewhereWithAmount';
+ translationKey = hasMissingInvoiceBankAccount(IOUReportID) ? 'iou.payerSettledWithMissingBankAccount' : 'iou.paidElsewhereWithAmount';
break;
case CONST.IOU.PAYMENT_TYPE.EXPENSIFY:
case CONST.IOU.PAYMENT_TYPE.VBBA:
@@ -7693,7 +7732,7 @@ function isValidReport(report?: OnyxEntry): boolean {
/**
* Check to see if we are a participant of this report.
*/
-function isReportParticipant(accountID: number, report: OnyxEntry): boolean {
+function isReportParticipant(accountID: number | undefined, report: OnyxEntry