= {
+- source: PropTypes.object.isRequired,
+- alt: PropTypes.string,
+- altColor: PropTypes.string,
+- height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+- width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+- style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
+- computeMaxWidth: PropTypes.func.isRequired,
+- contentWidth: PropTypes.number,
+- enableExperimentalPercentWidth: PropTypes.bool,
+- initialDimensions: imgDimensionsType,
+- onPress: PropTypes.func,
+- testID: PropTypes.string,
+- objectFit: PropTypes.string,
+- cachedNaturalDimensions: imgDimensionsType,
+- containerProps: PropTypes.object
+-};
+-
+-/**
+- * @ignore
+- */
+-IMGElement.propTypes = propTypes;
+-
+-/**
+- * @ignore
+- */
+-IMGElement.defaultProps = {
+- enableExperimentalPercentWidth: false,
+- computeMaxWidth: identity,
+- imagesInitialDimensions: defaultImageInitialDimensions,
+- style: {}
+-};
+-
+ export default IMGElement;
+diff --git a/node_modules/react-native-render-html/src/elements/useIMGElementState.ts b/node_modules/react-native-render-html/src/elements/useIMGElementState.ts
+index 6590d21..b603f26 100644
+--- a/node_modules/react-native-render-html/src/elements/useIMGElementState.ts
++++ b/node_modules/react-native-render-html/src/elements/useIMGElementState.ts
+@@ -63,6 +63,10 @@ function useImageNaturalDimensions(props: {
+ };
+ }
+
++function identity(arg: any) {
++ return arg;
++}
++
+ function useFetchedNaturalDimensions(props: {
+ cachedNaturalDimensions?: ImageDimensions;
+ source: ImageURISource;
+@@ -116,7 +120,7 @@ export default function useIMGElementState(
+ altColor,
+ source,
+ contentWidth,
+- computeMaxWidth,
++ computeMaxWidth = identity,
+ objectFit,
+ initialDimensions = defaultImageInitialDimensions,
+ cachedNaturalDimensions
+diff --git a/node_modules/react-native-render-html/src/elements/useImageSpecifiedDimensions.ts b/node_modules/react-native-render-html/src/elements/useImageSpecifiedDimensions.ts
+index 5d6271b..710c73f 100644
+--- a/node_modules/react-native-render-html/src/elements/useImageSpecifiedDimensions.ts
++++ b/node_modules/react-native-render-html/src/elements/useImageSpecifiedDimensions.ts
+@@ -71,8 +71,7 @@ function deriveSpecifiedDimensionsFromProps({
+ export default function useImageSpecifiedDimensions(
+ props: UseIMGElementStateProps
+ ) {
+- const { contentWidth, enableExperimentalPercentWidth, style, width, height } =
+- props;
++ const { contentWidth, enableExperimentalPercentWidth = false, style = {}, width, height } = props
+ const flatStyle = useMemo(() => StyleSheet.flatten(style) || {}, [style]);
+ const specifiedDimensions = useMemo(
+ () =>
+diff --git a/node_modules/react-native-render-html/src/index.ts b/node_modules/react-native-render-html/src/index.ts
+index 8569583..b59ec49 100644
+--- a/node_modules/react-native-render-html/src/index.ts
++++ b/node_modules/react-native-render-html/src/index.ts
+@@ -128,7 +128,6 @@ export {
+ export { default as TNodeRenderer } from './TNodeRenderer';
+ export {
+ default as TRenderEngineProvider,
+- defaultFallbackFonts,
+ useAmbientTRenderEngine
+ } from './TRenderEngineProvider';
+ export { default as RenderHTMLConfigProvider } from './RenderHTMLConfigProvider';
+diff --git a/node_modules/react-native-render-html/src/renderChildren.tsx b/node_modules/react-native-render-html/src/renderChildren.tsx
+index a669402..be9ffd6 100644
+--- a/node_modules/react-native-render-html/src/renderChildren.tsx
++++ b/node_modules/react-native-render-html/src/renderChildren.tsx
+@@ -4,8 +4,6 @@ import TNodeRenderer from './TNodeRenderer';
+ import { TChildrenRendererProps } from './shared-types';
+ import collapseTopMarginForChild from './helpers/collapseTopMarginForChild';
+
+-const empty = {};
+-
+ const mapCollapsibleChildren = (
+ propsForChildren: TChildrenRendererProps['propsForChildren'],
+ renderChild: TChildrenRendererProps['renderChild'],
+@@ -39,7 +37,7 @@ const mapCollapsibleChildren = (
+
+ export default function renderChildren({
+ tchildren,
+- propsForChildren = empty,
++ propsForChildren = {},
+ disableMarginCollapsing,
+ renderChild
+ }: TChildrenRendererProps): ReactElement {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 062da712cd7f..343523261369 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -895,7 +895,7 @@ const ROUTES = {
},
WORKSPACE_PROFILE_DESCRIPTION: {
route: 'settings/workspaces/:policyID/profile/description',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/profile/description` as const,
+ getRoute: (policyID: string | undefined) => `settings/workspaces/${policyID}/profile/description` as const,
},
WORKSPACE_PROFILE_SHARE: {
route: 'settings/workspaces/:policyID/profile/share',
@@ -1017,13 +1017,13 @@ const ROUTES = {
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}` as const,
},
WORKSPACE_UPGRADE: {
- route: 'settings/workspaces/:policyID/upgrade/:featureName?',
- getRoute: (policyID: string, featureName?: string, backTo?: string) =>
- getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo),
+ route: 'settings/workspaces/:policyID?/upgrade/:featureName?',
+ getRoute: (policyID?: string, featureName?: string, backTo?: string) =>
+ policyID ? getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo) : (`settings/workspaces/upgrade` as const),
},
WORKSPACE_DOWNGRADE: {
- route: 'settings/workspaces/:policyID/downgrade/',
- getRoute: (policyID: string) => `settings/workspaces/${policyID}/downgrade/` as const,
+ route: 'settings/workspaces/:policyID?/downgrade/',
+ getRoute: (policyID?: string) => (policyID ? (`settings/workspaces/${policyID}/downgrade/` as const) : `settings/workspaces/downgrade`),
},
WORKSPACE_CATEGORIES_SETTINGS: {
route: 'settings/workspaces/:policyID/categories/settings',
diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
index d211aac7fd4c..b4002767524f 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
@@ -49,12 +49,22 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
}),
comment: HTMLElementModel.fromCustomModel({
tagName: 'comment',
- mixedUAStyles: {whiteSpace: 'pre'},
+ getMixedUAStyles: (tnode) => {
+ if (tnode.attributes.islarge === undefined) {
+ return {whiteSpace: 'pre'};
+ }
+ return {whiteSpace: 'pre', ...styles.onlyEmojisText};
+ },
contentModel: HTMLContentModel.block,
}),
'email-comment': HTMLElementModel.fromCustomModel({
tagName: 'email-comment',
- mixedUAStyles: {whiteSpace: 'normal'},
+ getMixedUAStyles: (tnode) => {
+ if (tnode.attributes.islarge === undefined) {
+ return {whiteSpace: 'normal'};
+ }
+ return {whiteSpace: 'normal', ...styles.onlyEmojisText};
+ },
contentModel: HTMLContentModel.block,
}),
strong: HTMLElementModel.fromCustomModel({
@@ -102,6 +112,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
styles.textSupporting,
styles.textLineThrough,
styles.mutedNormalTextLabel,
+ styles.onlyEmojisText,
styles.onlyEmojisTextLineHeight,
],
);
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx
index 88e5c1f42555..e8d7e0e85fdf 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.tsx
@@ -12,9 +12,8 @@ function EditedRenderer({tnode, TDefaultRenderer, style, ...defaultRendererProps
const styles = useThemeStyles();
const {translate} = useLocalize();
const isPendingDelete = !!(tnode.attributes.deleted !== undefined);
- const isLarge = !!(tnode.attributes.islarge !== undefined);
return (
-
+
void;
if (isVisible) {
Modal.willAlertModalBecomeVisible(true, type === CONST.MODAL.MODAL_TYPE.POPOVER || type === CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED);
+ Keyboard.dismiss();
// To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu
removeOnCloseListener = Modal.setCloseModal(onClose);
}
diff --git a/src/components/RNMarkdownTextInput.tsx b/src/components/RNMarkdownTextInput.tsx
index e8ed0256bf0a..cd4cd86c349e 100644
--- a/src/components/RNMarkdownTextInput.tsx
+++ b/src/components/RNMarkdownTextInput.tsx
@@ -13,6 +13,17 @@ type AnimatedMarkdownTextInputRef = typeof AnimatedMarkdownTextInput & MarkdownT
type RNMarkdownTextInputProps = Omit;
+function handleFormatSelection(selectedText: string, formatCommand: string) {
+ switch (formatCommand) {
+ case 'formatBold':
+ return `*${selectedText}*`;
+ case 'formatItalic':
+ return `_${selectedText}_`;
+ default:
+ return selectedText;
+ }
+}
+
function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputProps, ref: ForwardedRef) {
const theme = useTheme();
@@ -28,6 +39,7 @@ function RNMarkdownTextInputWithRef({maxLength, ...props}: RNMarkdownTextInputPr
}
ref(refHandle as AnimatedMarkdownTextInputRef);
}}
+ formatSelection={handleFormatSelection}
// eslint-disable-next-line
{...props}
/**
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index f5e27896c8e4..03b6c820da00 100644
--- a/src/components/Search/index.tsx
+++ b/src/components/Search/index.tsx
@@ -520,6 +520,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
scrollEventThrottle={1}
shouldKeepFocusedItemAtTopOfViewableArea={type === CONST.SEARCH.DATA_TYPES.CHAT}
isScreenFocused={isSearchScreenFocused}
+ initialNumToRender={shouldUseNarrowLayout ? 5 : undefined}
/>
);
}
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index 4738bf6a92d8..f00454099a03 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -119,6 +119,7 @@ function BaseSelectionList(
shouldScrollToFocusedIndex = true,
onContentSizeChange,
listItemTitleStyles,
+ initialNumToRender = 12,
}: BaseSelectionListProps,
ref: ForwardedRef,
) {
@@ -754,14 +755,11 @@ function BaseSelectionList(
isTextInputFocusedRef.current = isTextInputFocused;
}, []);
- useImperativeHandle(ref, () => ({scrollAndHighlightItem, clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus, scrollToIndex, getFocusedOption}), [
- scrollAndHighlightItem,
- clearInputAfterSelect,
- updateAndScrollToFocusedIndex,
- updateExternalTextInputFocus,
- scrollToIndex,
- getFocusedOption,
- ]);
+ useImperativeHandle(
+ ref,
+ () => ({scrollAndHighlightItem, clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus, scrollToIndex, getFocusedOption, focusTextInput}),
+ [scrollAndHighlightItem, clearInputAfterSelect, updateAndScrollToFocusedIndex, updateExternalTextInputFocus, scrollToIndex, getFocusedOption, focusTextInput],
+ );
/** Selects row when pressing Enter */
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {
@@ -831,7 +829,7 @@ function BaseSelectionList(
indicatorStyle="white"
keyboardShouldPersistTaps="always"
showsVerticalScrollIndicator={showScrollIndicator}
- initialNumToRender={12}
+ initialNumToRender={initialNumToRender}
maxToRenderPerBatch={maxToRenderPerBatch}
windowSize={windowSize}
updateCellsBatchingPeriod={updateCellsBatchingPeriod}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 19c47414b089..213fd71d0632 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -633,6 +633,9 @@ type BaseSelectionListProps = Partial & {
/** Called when scrollable content view of the ScrollView changes */
onContentSizeChange?: (w: number, h: number) => void;
+
+ /** Initial number of items to render */
+ initialNumToRender?: number;
} & TRightHandSideComponent;
type SelectionListHandle = {
@@ -642,6 +645,7 @@ type SelectionListHandle = {
updateAndScrollToFocusedIndex: (newFocusedIndex: number) => void;
updateExternalTextInputFocus: (isTextInputFocused: boolean) => void;
getFocusedOption: () => ListItem | undefined;
+ focusTextInput: () => void;
};
type ItemLayout = {
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx
index 3df4a914f2d9..b0d9bcc44485 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.tsx
+++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx
@@ -342,8 +342,21 @@ function BaseVideoPlayer({
shareVideoPlayerElements(videoPlayerRef.current, videoPlayerElementParentRef.current, videoPlayerElementRef.current, isUploading || isFullScreenRef.current);
}, [currentlyPlayingURL, shouldUseSharedVideoElement, shareVideoPlayerElements, url, isUploading, isFullScreenRef]);
+ // Call bindFunctions() through the refs to avoid adding it to the dependency array of the DOM mutation effect, as doing so would change the DOM when the functions update.
+ const bindFunctionsRef = useRef<(() => void) | null>(null);
+ const shouldBindFunctionsRef = useRef(false);
+
+ useEffect(() => {
+ bindFunctionsRef.current = bindFunctions;
+ if (shouldBindFunctionsRef.current) {
+ bindFunctions();
+ }
+ }, [bindFunctions]);
+
// append shared video element to new parent (used for example in attachment modal)
useEffect(() => {
+ shouldBindFunctionsRef.current = false;
+
if (url !== currentlyPlayingURL || !sharedElement || isFullScreenRef.current) {
return;
}
@@ -360,7 +373,8 @@ function BaseVideoPlayer({
videoPlayerRef.current = currentVideoPlayerRef.current;
if (currentlyPlayingURL === url && newParentRef && 'appendChild' in newParentRef) {
newParentRef.appendChild(sharedElement as HTMLDivElement);
- bindFunctions();
+ bindFunctionsRef.current?.();
+ shouldBindFunctionsRef.current = true;
}
return () => {
if (!originalParent || !('appendChild' in originalParent)) {
@@ -373,7 +387,7 @@ function BaseVideoPlayer({
}
newParentRef.childNodes[0]?.remove();
};
- }, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, isFullScreenRef, originalParent, sharedElement, shouldUseSharedVideoElement, url]);
+ }, [currentVideoPlayerRef, currentlyPlayingURL, isFullScreenRef, originalParent, sharedElement, shouldUseSharedVideoElement, url]);
useEffect(() => {
if (!shouldPlay) {
diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts
index a1a1ed2c00e5..a626606bf3e0 100644
--- a/src/hooks/useOnboardingFlow.ts
+++ b/src/hooks/useOnboardingFlow.ts
@@ -16,6 +16,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
* Warning: This hook should be used only once in the app
*/
function useOnboardingFlowRouter() {
+ const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {initialValue: true});
const [isOnboardingCompleted, isOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {
selector: hasCompletedGuidedSetupFlowSelector,
});
@@ -32,6 +33,10 @@ function useOnboardingFlowRouter() {
useEffect(() => {
// This should delay opening the onboarding modal so it does not interfere with the ongoing ReportScreen params changes
InteractionManager.runAfterInteractions(() => {
+ if (isLoadingApp !== false) {
+ return;
+ }
+
if (isLoadingOnyxValue(isOnboardingCompletedMetadata, tryNewDotdMetadata, dismissedProductTrainingMetadata)) {
return;
}
@@ -76,6 +81,7 @@ function useOnboardingFlowRouter() {
}
});
}, [
+ isLoadingApp,
isOnboardingCompleted,
isHybridAppOnboardingCompleted,
isOnboardingCompletedMetadata,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 1ff4ef4c0ae4..3c762a0d8dd5 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -523,6 +523,7 @@ const translations = {
chooseDocument: 'Choose file',
attachmentTooLarge: 'Attachment is too large',
sizeExceeded: 'Attachment size is larger than 24 MB limit',
+ sizeExceededWithLimit: ({maxUploadSizeInMB}: SizeExceededParams) => `Attachment size is larger than ${maxUploadSizeInMB} MB limit`,
attachmentTooSmall: 'Attachment is too small',
sizeNotMet: 'Attachment size must be greater than 240 bytes',
wrongFileType: 'Invalid file type',
@@ -2575,6 +2576,7 @@ const translations = {
notAuthorized: `You don't have access to this page. If you're trying to join this workspace, just ask the workspace owner to add you as a member. Something else? Reach out to ${CONST.EMAIL.CONCIERGE}.`,
goToRoom: ({roomName}: GoToRoomParams) => `Go to ${roomName} room`,
goToWorkspace: 'Go to workspace',
+ goToWorkspaces: 'Go to workspaces',
clearFilter: 'Clear filter',
workspaceName: 'Workspace name',
workspaceOwner: 'Owner',
@@ -4383,7 +4385,8 @@ const translations = {
title: 'Upgrade to the Control plan',
note: 'Unlock our most powerful features, including:',
benefits: {
- note: 'The Control plan starts at $9 per active member per month.',
+ startsAt: 'The Control plan starts at ',
+ perMember: 'per active member per month.',
learnMore: 'Learn more',
pricing: 'about our plans and pricing.',
benefit1: 'Advanced accounting connections (NetSuite, Sage Intacct, and more)',
@@ -4410,7 +4413,7 @@ const translations = {
},
completed: {
headline: 'Your workspace has been downgraded',
- description: 'You have other workspace on the Control plan. To be billed at the Collect rate, you must downgrade all workspaces.',
+ description: 'You have other workspaces on the Control plan. To be billed at the Collect rate, you must downgrade all workspaces.',
gotIt: 'Got it, thanks',
},
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 92e94446aa48..bf8205af49d7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -518,6 +518,7 @@ const translations = {
chooseDocument: 'Elegir un archivo',
attachmentTooLarge: 'Archivo adjunto demasiado grande',
sizeExceeded: 'El archivo adjunto supera el límite de 24 MB.',
+ sizeExceededWithLimit: ({maxUploadSizeInMB}: SizeExceededParams) => `El archivo adjunto supera el límite de ${maxUploadSizeInMB} MB.`,
attachmentTooSmall: 'Archivo adjunto demasiado pequeño',
sizeNotMet: 'El archivo adjunto debe ser más grande que 240 bytes.',
wrongFileType: 'Tipo de archivo inválido',
@@ -2598,6 +2599,7 @@ const translations = {
notAuthorized: `No tienes acceso a esta página. Si estás intentando unirte a este espacio de trabajo, pide al dueño del espacio de trabajo que te añada como miembro. ¿Necesitas algo más? Comunícate con ${CONST.EMAIL.CONCIERGE}`,
goToRoom: ({roomName}: GoToRoomParams) => `Ir a la sala ${roomName}`,
goToWorkspace: 'Ir al espacio de trabajo',
+ goToWorkspaces: 'Ir a espacios de trabajo',
clearFilter: 'Borrar filtro',
workspaceName: 'Nombre del espacio de trabajo',
workspaceOwner: 'Dueño',
@@ -4449,7 +4451,8 @@ const translations = {
title: 'Mejorar al plan Controlar',
note: 'Desbloquea nuestras funciones más potentes, incluyendo:',
benefits: {
- note: 'El plan Controlar comienza desde $9 por miembro activo al mes.',
+ startsAt: 'El plan Controlar comienza desde ',
+ perMember: 'por miembro activo al mes.',
learnMore: 'Más información',
pricing: 'sobre nuestros planes y precios.',
benefit1: 'Conexiones avanzadas de contabilidad (NetSuite, Sage Intacct y más)',
@@ -4476,7 +4479,7 @@ const translations = {
},
completed: {
headline: 'Tu espacio de trabajo ha sido bajado de categoría',
- description: 'Tienes otro espacio de trabajo en el plan Controlar. Para facturarte con la tasa del plan Recopilar, debes bajar de categoría todos los espacios de trabajo.',
+ description: 'Tienes otros espacios de trabajo en el plan Controlar. Para facturarte con la tasa del plan Recopilar, debes bajar de categoría todos los espacios de trabajo.',
gotIt: 'Entendido, gracias.',
},
},
diff --git a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
index 78eb0adecc5e..d6d8f4169d76 100644
--- a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
+++ b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
@@ -1,5 +1,3 @@
-import type {Receipt} from '@src/types/onyx/Transaction';
-
type CategorizeTrackedExpenseParams = {
amount: number;
currency: string;
@@ -16,7 +14,6 @@ type CategorizeTrackedExpenseParams = {
reportPreviewReportActionID: string;
category?: string;
tag?: string;
- receipt?: Receipt;
taxCode: string;
taxAmount: number;
billable?: boolean;
diff --git a/src/libs/API/parameters/ConvertTrackedExpenseToRequestParams.ts b/src/libs/API/parameters/ConvertTrackedExpenseToRequestParams.ts
index c51161b043a8..2942923f6b37 100644
--- a/src/libs/API/parameters/ConvertTrackedExpenseToRequestParams.ts
+++ b/src/libs/API/parameters/ConvertTrackedExpenseToRequestParams.ts
@@ -1,5 +1,3 @@
-import type {Receipt} from '@src/types/onyx/Transaction';
-
type ConvertTrackedExpenseToRequestParams = {
amount: number;
currency: string;
@@ -11,7 +9,6 @@ type ConvertTrackedExpenseToRequestParams = {
transactionID: string;
actionableWhisperReportActionID: string;
createdChatReportActionID: string;
- receipt?: Receipt;
moneyRequestReportID: string;
moneyRequestCreatedReportActionID: string;
moneyRequestPreviewReportActionID: string;
diff --git a/src/libs/API/parameters/ShareTrackedExpenseParams.ts b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
index cee4bc40d9ac..dc95d211427b 100644
--- a/src/libs/API/parameters/ShareTrackedExpenseParams.ts
+++ b/src/libs/API/parameters/ShareTrackedExpenseParams.ts
@@ -1,5 +1,3 @@
-import type {Receipt} from '@src/types/onyx/Transaction';
-
type ShareTrackedExpenseParams = {
amount: number;
currency: string;
@@ -16,7 +14,6 @@ type ShareTrackedExpenseParams = {
reportPreviewReportActionID: string;
category?: string;
tag?: string;
- receipt?: Receipt;
taxCode: string;
taxAmount: number;
billable?: boolean;
diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts
index 207977ddb000..79ed264ade68 100644
--- a/src/libs/CategoryUtils.ts
+++ b/src/libs/CategoryUtils.ts
@@ -36,7 +36,7 @@ function formatRequireReceiptsOverText(translate: LocaleContextProps['translate'
return translate(`workspace.rules.categoryRules.requireReceiptsOverList.never`);
}
- const maxExpenseAmountToDisplay = policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE ? 0 : policy?.maxExpenseAmount;
+ const maxExpenseAmountToDisplay = policy?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE ? 0 : policy?.maxExpenseAmountNoReceipt;
return translate(`workspace.rules.categoryRules.requireReceiptsOverList.default`, {
defaultAmount: CurrencyUtils.convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD),
diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts
index ad4cfb31f4d3..930c12241b78 100644
--- a/src/libs/Fullstory/index.native.ts
+++ b/src/libs/Fullstory/index.native.ts
@@ -1,5 +1,6 @@
import FullStory, {FSPage} from '@fullstory/react-native';
import type {OnyxEntry} from 'react-native-onyx';
+import {isExpensifyTeam} from '@libs/PolicyUtils';
import {isConciergeChatReport, shouldUnmaskChat} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import * as Environment from '@src/libs/Environment/Environment';
@@ -42,7 +43,7 @@ const FS = {
// UserMetadata onyx key.
Environment.getEnvironment().then((envName: string) => {
const isTestEmail = value.email !== undefined && value.email.startsWith('fullstory') && value.email.endsWith(CONST.EMAIL.QA_DOMAIN);
- if (CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) {
+ if ((CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) || isExpensifyTeam(value?.email)) {
return;
}
FullStory.restart();
diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts
index 39d2d7e310e5..c7b1c2c9eb7a 100644
--- a/src/libs/Fullstory/index.ts
+++ b/src/libs/Fullstory/index.ts
@@ -1,5 +1,6 @@
import {FullStory, init, isInitialized} from '@fullstory/browser';
import type {OnyxEntry} from 'react-native-onyx';
+import {isExpensifyTeam} from '@libs/PolicyUtils';
import {isConciergeChatReport, shouldUnmaskChat} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import * as Environment from '@src/libs/Environment/Environment';
@@ -129,7 +130,7 @@ const FS = {
try {
Environment.getEnvironment().then((envName: string) => {
const isTestEmail = value.email !== undefined && value.email.startsWith('fullstory') && value.email.endsWith(CONST.EMAIL.QA_DOMAIN);
- if (CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) {
+ if ((CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) || isExpensifyTeam(value?.email)) {
return;
}
FS.onReady().then(() => {
diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts
index 66ce71451c17..ad3ae653daa1 100644
--- a/src/libs/HttpUtils.ts
+++ b/src/libs/HttpUtils.ts
@@ -10,6 +10,10 @@ import * as UpdateRequired from './actions/UpdateRequired';
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from './API/types';
import * as ApiUtils from './ApiUtils';
import HttpsError from './Errors/HttpsError';
+import getPlatform from './getPlatform';
+
+const platform = getPlatform();
+const isNativePlatform = platform === CONST.PLATFORM.ANDROID || platform === CONST.PLATFORM.IOS;
let shouldFailAllRequests = false;
let shouldForceOffline = false;
@@ -160,10 +164,12 @@ function processHTTPRequest(url: string, method: RequestType = 'get', body: Form
function xhr(command: string, data: Record, type: RequestType = CONST.NETWORK.METHOD.POST, shouldUseSecure = false): Promise {
const formData = new FormData();
Object.keys(data).forEach((key) => {
- if (typeof data[key] === 'undefined') {
+ const value = data[key];
+ if (value === undefined) {
return;
}
- formData.append(key, data[key] as string | Blob);
+ validateFormDataParameter(command, key, value);
+ formData.append(key, value as string | Blob);
});
const url = ApiUtils.getCommandURL({shouldUseSecure, command});
@@ -172,6 +178,35 @@ function xhr(command: string, data: Record, type: RequestType =
return processHTTPRequest(url, type, formData, abortSignalController?.signal);
}
+/**
+ * Ensures no value of type `object` other than null, Blob, its subclasses, or {uri: string} (native platforms only) is passed to XMLHttpRequest.
+ * Otherwise, it will be incorrectly serialized as `[object Object]` and cause an error on Android.
+ * See https://github.com/Expensify/App/issues/45086
+ */
+function validateFormDataParameter(command: string, key: string, value: unknown) {
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ const isValid = (value: unknown, isTopLevel: boolean): boolean => {
+ if (value === null || typeof value !== 'object') {
+ return true;
+ }
+ if (Array.isArray(value)) {
+ return value.every((element) => isValid(element, false));
+ }
+ if (isTopLevel) {
+ // Native platforms only require the value to include the `uri` property.
+ // Optionally, it can also have a `name` and `type` props.
+ // On other platforms, the value must be an instance of `Blob`.
+ return isNativePlatform ? 'uri' in value && !!value.uri : value instanceof Blob;
+ }
+ return false;
+ };
+
+ if (!isValid(value, true)) {
+ // eslint-disable-next-line no-console
+ console.warn(`An unsupported value was passed to command '${command}' (parameter: '${key}'). Only Blob and primitive types are allowed.`);
+ }
+}
+
function cancelPendingRequests(command: AbortCommand = ABORT_COMMANDS.All) {
const controller = abortControllerMap.get(command);
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index 9a03409fb3a2..2c3b060e0835 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -9,7 +9,6 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.CURRENCY,
SCREENS.WORKSPACE.DESCRIPTION,
SCREENS.WORKSPACE.SHARE,
- SCREENS.WORKSPACE.DOWNGRADE,
],
[SCREENS.WORKSPACE.MEMBERS]: [
SCREENS.WORKSPACE.MEMBER_DETAILS,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index ffed2519f775..088f624c7dc3 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -247,13 +247,13 @@ type SettingsNavigatorParamList = {
backTo?: Routes;
};
[SCREENS.WORKSPACE.UPGRADE]: {
- policyID: string;
+ policyID?: string;
featureName?: string;
backTo?: Routes;
categoryId?: string;
};
[SCREENS.WORKSPACE.DOWNGRADE]: {
- policyID: string;
+ policyID?: string;
};
[SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: {
policyID: string;
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 4d1988a53bde..43f601b167aa 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -111,7 +111,7 @@ type GetOptionsConfig = {
};
type GetUserToInviteConfig = {
- searchValue: string;
+ searchValue: string | undefined;
optionsToExclude?: Array>;
reportActions?: ReportActions;
shouldAcceptName?: boolean;
@@ -1078,6 +1078,10 @@ function getUserToInviteOption({
showChatPreviewLine = false,
shouldAcceptName = false,
}: GetUserToInviteConfig): ReportUtils.OptionData | null {
+ if (!searchValue) {
+ return null;
+ }
+
const parsedPhoneNumber = PhoneNumber.parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchValue)));
const isCurrentUserLogin = isCurrentUser({login: searchValue} as PersonalDetails);
const isInSelectedOption = selectedOptions.some((option) => 'login' in option && option.login === searchValue);
@@ -1086,7 +1090,7 @@ function getUserToInviteOption({
const isInOptionToExclude =
optionsToExclude.findIndex((optionToExclude) => 'login' in optionToExclude && optionToExclude.login === PhoneNumber.addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) !== -1;
- if (!searchValue || isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber && !shouldAcceptName) || isInOptionToExclude) {
+ if (isCurrentUserLogin || isInSelectedOption || (!isValidEmail && !isValidPhoneNumber && !shouldAcceptName) || isInOptionToExclude) {
return null;
}
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 2a4168a24668..672e79ff335f 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -34,6 +34,7 @@ import type PolicyEmployee from '@src/types/onyx/PolicyEmployee';
import type {SearchPolicy} from '@src/types/onyx/SearchResults';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {hasSynchronizationErrorMessage} from './actions/connections';
+import {getCurrentUserAccountID} from './actions/Report';
import {getCategoryApproverRule} from './CategoryUtils';
import * as Localize from './Localize';
import Navigation from './Navigation/Navigation';
@@ -687,11 +688,6 @@ function canSendInvoice(policies: OnyxCollection | null, currentUserLogi
return getActiveAdminWorkspaces(policies, currentUserLogin).some((policy) => canSendInvoiceFromWorkspace(policy.id));
}
-function hasWorkspaceWithInvoices(currentUserLogin: string | undefined): boolean {
- const activePolicies = getActivePolicies(allPolicies, currentUserLogin);
- return activePolicies.some((policy) => shouldShowPolicy(policy, NetworkStore.isOffline(), currentUserLogin) && policy.areInvoicesEnabled);
-}
-
function hasDependentTags(policy: OnyxEntry, policyTagList: OnyxEntry) {
if (!policy?.hasMultipleTagLists) {
return false;
@@ -1190,6 +1186,25 @@ function hasOtherControlWorkspaces(currentPolicyID: string) {
return otherControlWorkspaces.length > 0;
}
+// If no policyID is provided, it indicates the workspace upgrade/downgrade URL
+// is being accessed from the Subscriptions page without a specific policyID.
+// In this case, check if the user is an admin on more than one policy.
+// If the user is an admin for multiple policies, we can render the page as it contains a condition
+// to navigate them to the Workspaces page when no policyID is provided, instead of showing the Upgrade/Downgrade button.
+// If the user is not an admin for multiple policies, they are not allowed to perform this action, and the NotFoundPage is displayed.
+
+function canModifyPlan(policyID?: string) {
+ const currentUserAccountID = getCurrentUserAccountID();
+ const ownerPolicies = getOwnedPaidPolicies(allPolicies, currentUserAccountID);
+
+ if (!policyID) {
+ return ownerPolicies.length > 1;
+ }
+ const policy = getPolicy(policyID);
+
+ return !!policy && isPolicyAdmin(policy);
+}
+
export {
canEditTaxRate,
extractPolicyIDFromPath,
@@ -1249,7 +1264,6 @@ export {
canSendInvoiceFromWorkspace,
canSubmitPerDiemExpenseFromWorkspace,
canSendInvoice,
- hasWorkspaceWithInvoices,
hasDependentTags,
hasVBBA,
getXeroTenants,
@@ -1318,6 +1332,7 @@ export {
hasOtherControlWorkspaces,
getManagerAccountEmail,
getRuleApprovers,
+ canModifyPlan,
};
export type {MemberEmailsToAccountIDs};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 8a5ae8b1d102..67e0828f5c61 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -4957,7 +4957,7 @@ function getRejectedReportMessage() {
function getWorkspaceNameUpdatedMessage(action: ReportAction) {
const {oldName, newName} = ReportActionsUtils.getOriginalMessage(action as ReportAction) ?? {};
const message = oldName && newName ? Localize.translateLocal('workspaceActions.renamedWorkspaceNameAction', {oldName, newName}) : ReportActionsUtils.getReportActionText(action);
- return message;
+ return Str.htmlEncode(message);
}
/**
@@ -8349,13 +8349,11 @@ function createDraftTransactionAndNavigateToParticipantSelector(
mccGroup,
} as Transaction);
- const filteredPolicies = Object.values(allPolicies ?? {}).filter(
- (policy) => policy && policy.type !== CONST.POLICY.TYPE.PERSONAL && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
- );
+ const filteredPolicies = Object.values(allPolicies ?? {}).filter((policy) => PolicyUtils.shouldShowPolicy(policy, false, currentUserEmail));
if (actionName === CONST.IOU.ACTION.CATEGORIZE) {
const activePolicy = getPolicy(activePolicyID);
- if (activePolicy && activePolicy?.type !== CONST.POLICY.TYPE.PERSONAL && activePolicy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ if (PolicyUtils.shouldShowPolicy(activePolicy, false, currentUserEmail)) {
const policyExpenseReportID = getPolicyExpenseChat(currentUserAccountID, activePolicyID)?.reportID;
IOU.setMoneyRequestParticipants(transactionID, [
{
diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js
index 0b456b5823b1..d8705428bc1f 100644
--- a/src/libs/SearchParser/autocompleteParser.js
+++ b/src/libs/SearchParser/autocompleteParser.js
@@ -212,13 +212,13 @@ function peg$parse(input, options) {
var peg$c34 = ">";
var peg$c35 = "<=";
var peg$c36 = "<";
- var peg$c37 = "\"";
var peg$r0 = /^[:=]/;
- var peg$r1 = /^[^ ,"\t\n\r]/;
- var peg$r2 = /^[^"\r\n]/;
- var peg$r3 = /^[^ ,\t\n\r]/;
- var peg$r4 = /^[ \t\r\n]/;
+ var peg$r1 = /^[^ ,"\u201D\u201C\t\n\r]/;
+ var peg$r2 = /^["\u201C-\u201D]/;
+ var peg$r3 = /^[^"\u201D\u201C\r\n]/;
+ var peg$r4 = /^[^ ,\t\n\r]/;
+ var peg$r5 = /^[ \t\r\n]/;
var peg$e0 = peg$literalExpectation(",", false);
var peg$e1 = peg$otherExpectation("key");
@@ -261,9 +261,9 @@ function peg$parse(input, options) {
var peg$e38 = peg$literalExpectation("<=", false);
var peg$e39 = peg$literalExpectation("<", false);
var peg$e40 = peg$otherExpectation("quote");
- var peg$e41 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r"], true, false);
- var peg$e42 = peg$literalExpectation("\"", false);
- var peg$e43 = peg$classExpectation(["\"", "\r", "\n"], true, false);
+ var peg$e41 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r"], true, false);
+ var peg$e42 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false);
+ var peg$e43 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false);
var peg$e44 = peg$classExpectation([" ", ",", "\t", "\n", "\r"], true, false);
var peg$e45 = peg$otherExpectation("word");
var peg$e46 = peg$otherExpectation("whitespace");
@@ -1418,8 +1418,8 @@ function peg$parse(input, options) {
if (peg$silentFails === 0) { peg$fail(peg$e41); }
}
}
- if (input.charCodeAt(peg$currPos) === 34) {
- s2 = peg$c37;
+ s2 = input.charAt(peg$currPos);
+ if (peg$r2.test(s2)) {
peg$currPos++;
} else {
s2 = peg$FAILED;
@@ -1428,7 +1428,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = [];
s4 = input.charAt(peg$currPos);
- if (peg$r2.test(s4)) {
+ if (peg$r3.test(s4)) {
peg$currPos++;
} else {
s4 = peg$FAILED;
@@ -1437,15 +1437,15 @@ function peg$parse(input, options) {
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = input.charAt(peg$currPos);
- if (peg$r2.test(s4)) {
+ if (peg$r3.test(s4)) {
peg$currPos++;
} else {
s4 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e43); }
}
}
- if (input.charCodeAt(peg$currPos) === 34) {
- s4 = peg$c37;
+ s4 = input.charAt(peg$currPos);
+ if (peg$r2.test(s4)) {
peg$currPos++;
} else {
s4 = peg$FAILED;
@@ -1454,7 +1454,7 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = [];
s6 = input.charAt(peg$currPos);
- if (peg$r3.test(s6)) {
+ if (peg$r4.test(s6)) {
peg$currPos++;
} else {
s6 = peg$FAILED;
@@ -1463,7 +1463,7 @@ function peg$parse(input, options) {
while (s6 !== peg$FAILED) {
s5.push(s6);
s6 = input.charAt(peg$currPos);
- if (peg$r3.test(s6)) {
+ if (peg$r4.test(s6)) {
peg$currPos++;
} else {
s6 = peg$FAILED;
@@ -1496,7 +1496,7 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = [];
s2 = input.charAt(peg$currPos);
- if (peg$r3.test(s2)) {
+ if (peg$r4.test(s2)) {
peg$currPos++;
} else {
s2 = peg$FAILED;
@@ -1506,7 +1506,7 @@ function peg$parse(input, options) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = input.charAt(peg$currPos);
- if (peg$r3.test(s2)) {
+ if (peg$r4.test(s2)) {
peg$currPos++;
} else {
s2 = peg$FAILED;
@@ -1548,7 +1548,7 @@ function peg$parse(input, options) {
peg$silentFails++;
s0 = [];
s1 = input.charAt(peg$currPos);
- if (peg$r4.test(s1)) {
+ if (peg$r5.test(s1)) {
peg$currPos++;
} else {
s1 = peg$FAILED;
@@ -1557,7 +1557,7 @@ function peg$parse(input, options) {
while (s1 !== peg$FAILED) {
s0.push(s1);
s1 = input.charAt(peg$currPos);
- if (peg$r4.test(s1)) {
+ if (peg$r5.test(s1)) {
peg$currPos++;
} else {
s1 = peg$FAILED;
diff --git a/src/libs/SearchParser/baseRules.peggy b/src/libs/SearchParser/baseRules.peggy
index cc1305adc8b3..ebde336a6ead 100644
--- a/src/libs/SearchParser/baseRules.peggy
+++ b/src/libs/SearchParser/baseRules.peggy
@@ -56,7 +56,7 @@ operator "operator"
/ "<" { return "lt"; }
quotedString "quote"
- = start:[^ ,"\t\n\r]* "\"" inner:[^"\r\n]* "\"" end:[^ ,\t\n\r]* {
+ = start:[^ ,"”“\t\n\r]* ("“" / "\"" / "”") inner:[^"”“\r\n]* ("“" / "\"" / "”") end:[^ ,\t\n\r]* {
return [...start, '"', ...inner, '"', ...end].join("");
}
diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js
index 941ac7f59797..d9baeadb2a14 100644
--- a/src/libs/SearchParser/searchParser.js
+++ b/src/libs/SearchParser/searchParser.js
@@ -217,14 +217,14 @@ function peg$parse(input, options) {
var peg$c34 = ">";
var peg$c35 = "<=";
var peg$c36 = "<";
- var peg$c37 = "\"";
var peg$r0 = /^[^ \t\r\n]/;
var peg$r1 = /^[:=]/;
- var peg$r2 = /^[^ ,"\t\n\r]/;
- var peg$r3 = /^[^"\r\n]/;
- var peg$r4 = /^[^ ,\t\n\r]/;
- var peg$r5 = /^[ \t\r\n]/;
+ var peg$r2 = /^[^ ,"\u201D\u201C\t\n\r]/;
+ var peg$r3 = /^["\u201C-\u201D]/;
+ var peg$r4 = /^[^"\u201D\u201C\r\n]/;
+ var peg$r5 = /^[^ ,\t\n\r]/;
+ var peg$r6 = /^[ \t\r\n]/;
var peg$e0 = peg$classExpectation([" ", "\t", "\r", "\n"], true, false);
var peg$e1 = peg$otherExpectation("key");
@@ -269,9 +269,9 @@ function peg$parse(input, options) {
var peg$e40 = peg$literalExpectation("<=", false);
var peg$e41 = peg$literalExpectation("<", false);
var peg$e42 = peg$otherExpectation("quote");
- var peg$e43 = peg$classExpectation([" ", ",", "\"", "\t", "\n", "\r"], true, false);
- var peg$e44 = peg$literalExpectation("\"", false);
- var peg$e45 = peg$classExpectation(["\"", "\r", "\n"], true, false);
+ var peg$e43 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r"], true, false);
+ var peg$e44 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false);
+ var peg$e45 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false);
var peg$e46 = peg$classExpectation([" ", ",", "\t", "\n", "\r"], true, false);
var peg$e47 = peg$otherExpectation("word");
var peg$e48 = peg$otherExpectation("whitespace");
@@ -1616,8 +1616,8 @@ function peg$parse(input, options) {
if (peg$silentFails === 0) { peg$fail(peg$e43); }
}
}
- if (input.charCodeAt(peg$currPos) === 34) {
- s2 = peg$c37;
+ s2 = input.charAt(peg$currPos);
+ if (peg$r3.test(s2)) {
peg$currPos++;
} else {
s2 = peg$FAILED;
@@ -1626,7 +1626,7 @@ function peg$parse(input, options) {
if (s2 !== peg$FAILED) {
s3 = [];
s4 = input.charAt(peg$currPos);
- if (peg$r3.test(s4)) {
+ if (peg$r4.test(s4)) {
peg$currPos++;
} else {
s4 = peg$FAILED;
@@ -1635,15 +1635,15 @@ function peg$parse(input, options) {
while (s4 !== peg$FAILED) {
s3.push(s4);
s4 = input.charAt(peg$currPos);
- if (peg$r3.test(s4)) {
+ if (peg$r4.test(s4)) {
peg$currPos++;
} else {
s4 = peg$FAILED;
if (peg$silentFails === 0) { peg$fail(peg$e45); }
}
}
- if (input.charCodeAt(peg$currPos) === 34) {
- s4 = peg$c37;
+ s4 = input.charAt(peg$currPos);
+ if (peg$r3.test(s4)) {
peg$currPos++;
} else {
s4 = peg$FAILED;
@@ -1652,7 +1652,7 @@ function peg$parse(input, options) {
if (s4 !== peg$FAILED) {
s5 = [];
s6 = input.charAt(peg$currPos);
- if (peg$r4.test(s6)) {
+ if (peg$r5.test(s6)) {
peg$currPos++;
} else {
s6 = peg$FAILED;
@@ -1661,7 +1661,7 @@ function peg$parse(input, options) {
while (s6 !== peg$FAILED) {
s5.push(s6);
s6 = input.charAt(peg$currPos);
- if (peg$r4.test(s6)) {
+ if (peg$r5.test(s6)) {
peg$currPos++;
} else {
s6 = peg$FAILED;
@@ -1694,7 +1694,7 @@ function peg$parse(input, options) {
s0 = peg$currPos;
s1 = [];
s2 = input.charAt(peg$currPos);
- if (peg$r4.test(s2)) {
+ if (peg$r5.test(s2)) {
peg$currPos++;
} else {
s2 = peg$FAILED;
@@ -1704,7 +1704,7 @@ function peg$parse(input, options) {
while (s2 !== peg$FAILED) {
s1.push(s2);
s2 = input.charAt(peg$currPos);
- if (peg$r4.test(s2)) {
+ if (peg$r5.test(s2)) {
peg$currPos++;
} else {
s2 = peg$FAILED;
@@ -1746,7 +1746,7 @@ function peg$parse(input, options) {
peg$silentFails++;
s0 = [];
s1 = input.charAt(peg$currPos);
- if (peg$r5.test(s1)) {
+ if (peg$r6.test(s1)) {
peg$currPos++;
} else {
s1 = peg$FAILED;
@@ -1755,7 +1755,7 @@ function peg$parse(input, options) {
while (s1 !== peg$FAILED) {
s0.push(s1);
s1 = input.charAt(peg$currPos);
- if (peg$r5.test(s1)) {
+ if (peg$r6.test(s1)) {
peg$currPos++;
} else {
s1 = peg$FAILED;
diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts
index 73c83cb33b83..3aad00f815ba 100644
--- a/src/libs/SearchQueryUtils.ts
+++ b/src/libs/SearchQueryUtils.ts
@@ -67,11 +67,10 @@ const UserFriendlyKeyMap: Record {
+ preservedShouldUseStagingServer = value?.shouldUseStagingServer;
+ },
+});
+
const KEYS_TO_PRESERVE: OnyxKey[] = [
ONYXKEYS.ACCOUNT,
ONYXKEYS.IS_CHECKING_PUBLIC_ROOM,
@@ -540,6 +548,7 @@ function setPreservedUserSession(session: OnyxTypes.Session) {
function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) {
// The value of isUsingImportedState will be lost once Onyx is cleared, so we need to store it
const isStateImported = isUsingImportedState;
+ const shouldUseStagingServer = preservedShouldUseStagingServer;
const sequentialQueue = PersistedRequests.getAll();
Onyx.clear(KEYS_TO_PRESERVE).then(() => {
// Network key is preserved, so when using imported state, we should stop forcing offline mode so that the app can re-fetch the network
@@ -556,6 +565,10 @@ function clearOnyxAndResetApp(shouldNavigateToHomepage?: boolean) {
Onyx.set(ONYXKEYS.PRESERVED_USER_SESSION, null);
}
+ if (shouldUseStagingServer) {
+ Onyx.set(ONYXKEYS.USER, {shouldUseStagingServer});
+ }
+
// Requests in a sequential queue should be called even if the Onyx state is reset, so we do not lose any pending data.
// However, the OpenApp request must be called before any other request in a queue to ensure data consistency.
// To do that, sequential queue is cleared together with other keys, and then it's restored once the OpenApp request is resolved.
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index a7d27fea72de..980c9e79b1a5 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -126,7 +126,6 @@ type CategorizeTrackedExpenseTransactionParams = {
category?: string;
tag?: string;
billable?: boolean;
- receipt?: Receipt;
};
type CategorizeTrackedExpensePolicyParams = {
policyID: string;
@@ -3743,7 +3742,6 @@ function convertTrackedExpenseToRequest(
merchant: string,
created: string,
attendees?: Attendee[],
- receipt?: Receipt,
) {
const {optimisticData, successData, failureData} = onyxData;
@@ -3773,7 +3771,6 @@ function convertTrackedExpenseToRequest(
comment,
created,
merchant,
- receipt,
payerAccountID,
payerEmail,
chatReportID,
@@ -3815,10 +3812,10 @@ function categorizeTrackedExpense(trackedExpenseParams: CategorizeTrackedExpense
successData?.push(...moveTransactionSuccessData);
failureData?.push(...moveTransactionFailureData);
const parameters = {
- onyxData,
...reportInformation,
...policyParams,
...transactionParams,
+ linkedTrackedExpenseReportAction: undefined,
modifiedExpenseReportActionID,
policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID,
policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID,
@@ -3857,7 +3854,6 @@ function shareTrackedExpense(
taxCode = '',
taxAmount = 0,
billable?: boolean,
- receipt?: Receipt,
createdWorkspaceParams?: CreateWorkspaceParams,
) {
const {optimisticData, successData, failureData} = onyxData ?? {};
@@ -3900,7 +3896,6 @@ function shareTrackedExpense(
taxCode,
taxAmount,
billable,
- receipt,
policyExpenseChatReportID: createdWorkspaceParams?.expenseChatReportID,
policyExpenseCreatedReportActionID: createdWorkspaceParams?.expenseCreatedReportActionID,
adminsChatReportID: createdWorkspaceParams?.adminsChatReportID,
@@ -3993,7 +3988,6 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) {
merchant,
created,
attendees,
- receipt,
);
break;
}
@@ -4013,7 +4007,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation) {
createdChatReportActionID,
createdIOUReportActionID,
reportPreviewReportActionID: reportPreviewAction.reportActionID,
- receipt,
+ receipt: receipt instanceof Blob ? receipt : undefined,
receiptState: receipt?.state,
category,
tag,
@@ -4192,7 +4186,7 @@ function trackExpense(
if (!linkedTrackedExpenseReportAction || !actionableWhisperReportActionID || !linkedTrackedExpenseReportID) {
return;
}
- const transactionParams = {
+ const transactionParams: CategorizeTrackedExpenseTransactionParams = {
transactionID: transaction?.transactionID ?? '-1',
amount,
currency,
@@ -4204,13 +4198,12 @@ function trackExpense(
category,
tag,
billable,
- receipt: trackedReceipt,
};
- const policyParams = {
+ const policyParams: CategorizeTrackedExpensePolicyParams = {
policyID: chatReport?.policyID ?? '-1',
isDraftPolicy,
};
- const reportInformation = {
+ const reportInformation: CategorizeTrackedExpenseReportInformation = {
moneyRequestPreviewReportActionID: iouAction?.reportActionID ?? '-1',
moneyRequestReportID: iouReport?.reportID ?? '-1',
moneyRequestCreatedReportActionID: createdIOUReportActionID ?? '-1',
@@ -4220,7 +4213,7 @@ function trackExpense(
transactionThreadReportID: transactionThreadReportID ?? '-1',
reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '-1',
};
- const trackedExpenseParams = {
+ const trackedExpenseParams: CategorizeTrackedExpenseParams = {
onyxData,
reportInformation,
transactionParams,
@@ -4257,7 +4250,6 @@ function trackExpense(
taxCode,
taxAmount,
billable,
- trackedReceipt,
createdWorkspaceParams,
);
break;
@@ -4276,7 +4268,7 @@ function trackExpense(
createdChatReportActionID: createdChatReportActionID ?? '-1',
createdIOUReportActionID,
reportPreviewReportActionID: reportPreviewAction?.reportActionID,
- receipt: trackedReceipt,
+ receipt: trackedReceipt instanceof Blob ? trackedReceipt : undefined,
receiptState: trackedReceipt?.state,
category,
tag,
diff --git a/src/pages/ConciergePage.tsx b/src/pages/ConciergePage.tsx
index 0ce089787230..c4de1e3b4062 100644
--- a/src/pages/ConciergePage.tsx
+++ b/src/pages/ConciergePage.tsx
@@ -41,7 +41,7 @@ function ConciergePage() {
}
// Mark the viewTourTask as complete if we are redirected to Concierge after finishing the Navattic tour
- const {navattic} = route.params as {navattic?: string};
+ const {navattic} = (route.params as {navattic?: string}) ?? {};
if (navattic === CONST.NAVATTIC.COMPLETED) {
if (viewTourTaskReport) {
if (viewTourTaskReport.stateNum !== CONST.REPORT.STATE_NUM.APPROVED || viewTourTaskReport.statusNum !== CONST.REPORT.STATUS_NUM.APPROVED) {
diff --git a/src/pages/NewChatConfirmPage.tsx b/src/pages/NewChatConfirmPage.tsx
index 85e33cf0c598..360e76738381 100644
--- a/src/pages/NewChatConfirmPage.tsx
+++ b/src/pages/NewChatConfirmPage.tsx
@@ -55,7 +55,7 @@ function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmP
return [];
}
const options: Participant[] = newGroupDraft.participants.map((participant) =>
- OptionsListUtils.getParticipantsOption({accountID: participant.accountID, login: participant.login, reportID: ''}, allPersonalDetails),
+ OptionsListUtils.getParticipantsOption({accountID: participant.accountID, login: participant?.login, reportID: ''}, allPersonalDetails),
);
return options;
}, [allPersonalDetails, newGroupDraft?.participants]);
@@ -92,7 +92,7 @@ function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmP
if (!newGroupDraft) {
return;
}
- const newSelectedParticipants = (newGroupDraft.participants ?? []).filter((participant) => participant.login !== option.login);
+ const newSelectedParticipants = (newGroupDraft.participants ?? []).filter((participant) => participant?.login !== option.login);
Report.setGroupDraft({participants: newSelectedParticipants});
},
[newGroupDraft],
@@ -103,7 +103,7 @@ function NewChatConfirmPage({newGroupDraft, allPersonalDetails}: NewChatConfirmP
return;
}
- const logins: string[] = (newGroupDraft.participants ?? []).map((participant) => participant.login);
+ const logins: string[] = (newGroupDraft.participants ?? []).map((participant) => participant.login).filter((login): login is string => !!login);
Report.navigateToAndOpenReport(logins, true, undefined, newGroupDraft.reportName ?? '', newGroupDraft.avatarUri ?? '', avatarFile, optimisticReportID.current, true);
}, [newGroupDraft, avatarFile]);
diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx
index 65aa253ff09d..bcf0c917e80b 100755
--- a/src/pages/NewChatPage.tsx
+++ b/src/pages/NewChatPage.tsx
@@ -1,6 +1,7 @@
import isEmpty from 'lodash/isEmpty';
import reject from 'lodash/reject';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {Keyboard} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import ImportedStateIndicator from '@components/ImportedStateIndicator';
@@ -102,7 +103,7 @@ function useOptions() {
let participantOption: OptionData | undefined | null = listOptions.personalDetails.find((option) => option.accountID === participant.accountID);
if (!participantOption) {
participantOption = OptionsListUtils.getUserToInviteOption({
- searchValue: participant.login,
+ searchValue: participant?.login,
});
}
if (!participantOption) {
@@ -230,11 +231,13 @@ function NewChatPage() {
if (isOptionInList) {
newSelectedOptions = reject(selectedOptions, (selectedOption) => selectedOption.login === option.login);
} else {
- newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID ?? '-1'}];
+ newSelectedOptions = [...selectedOptions, {...option, isSelected: true, selected: true, reportID: option.reportID ?? `${CONST.DEFAULT_NUMBER_ID}`}];
}
selectionListRef?.current?.clearInputAfterSelect?.();
+ selectionListRef.current?.focusTextInput();
+ selectionListRef?.current?.scrollToIndex(Math.max(newSelectedOptions.length - 1, 0), true);
setSelectedOptions(newSelectedOptions);
}
@@ -272,9 +275,13 @@ function NewChatPage() {
if (!personalData || !personalData.login || !personalData.accountID) {
return;
}
- const selectedParticipants: SelectedParticipant[] = selectedOptions.map((option: OptionData) => ({login: option.login ?? '', accountID: option.accountID ?? -1}));
+ const selectedParticipants: SelectedParticipant[] = selectedOptions.map((option: OptionData) => ({
+ login: option?.login,
+ accountID: option.accountID ?? CONST.DEFAULT_NUMBER_ID,
+ }));
const logins = [...selectedParticipants, {login: personalData.login, accountID: personalData.accountID}];
Report.setGroupDraft({participants: logins});
+ Keyboard.dismiss();
Navigation.navigate(ROUTES.NEW_CHAT_CONFIRM);
}, [selectedOptions, personalData]);
const {isDismissed} = useDismissedReferralBanners({referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT});
diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx
index 166b12b27751..11f3f3c48efa 100644
--- a/src/pages/ReportDetailsPage.tsx
+++ b/src/pages/ReportDetailsPage.tsx
@@ -875,22 +875,15 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
// Where to navigate back to after deleting the transaction and its report.
const navigateToTargetUrl = useCallback(() => {
- let urlToNavigateBack: string | undefined;
-
+ // If transaction was not deleted (i.e. Cancel was clicked), do nothing
+ // which only dismiss the delete confirmation modal
if (!isTransactionDeleted.current) {
- if (caseID === CASES.DEFAULT) {
- urlToNavigateBack = Task.getNavigationUrlOnTaskDelete(report);
- if (urlToNavigateBack) {
- Report.setDeleteTransactionNavigateBackUrl(urlToNavigateBack);
- Navigation.goBack(urlToNavigateBack as Route);
- } else {
- Navigation.dismissModal();
- }
- return;
- }
return;
}
+ let urlToNavigateBack: string | undefined;
+
+ // Only proceed with navigation logic if transaction was actually deleted
if (!isEmptyObject(requestParentReportAction)) {
const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction);
if (isTrackExpense) {
@@ -906,7 +899,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta
Report.setDeleteTransactionNavigateBackUrl(urlToNavigateBack);
ReportUtils.navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true);
}
- }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView, isTransactionDeleted]);
+ }, [iouTransactionID, requestParentReportAction, isSingleTransactionView, isTransactionDeleted, moneyRequestReport?.reportID]);
const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]);
diff --git a/src/pages/Search/AdvancedSearchFilters.tsx b/src/pages/Search/AdvancedSearchFilters.tsx
index 10c8401b98aa..ccb4bbfe5da1 100644
--- a/src/pages/Search/AdvancedSearchFilters.tsx
+++ b/src/pages/Search/AdvancedSearchFilters.tsx
@@ -372,7 +372,7 @@ function AdvancedSearchFilters() {
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES);
const [searchAdvancedFilters = {} as SearchAdvancedFiltersForm] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM);
- const policyID = searchAdvancedFilters.policyID ?? '-1';
+ const policyID = searchAdvancedFilters.policyID;
const [cardList = {}] = useOnyx(ONYXKEYS.CARD_LIST);
const taxRates = getAllTaxRates();
const personalDetails = usePersonalDetails();
@@ -434,8 +434,8 @@ function AdvancedSearchFilters() {
const onSaveSearch = () => {
const savedSearchKeys = Object.keys(savedSearches ?? {});
if (!queryJSON || (savedSearches && savedSearchKeys.includes(String(queryJSON.hash)))) {
- // If the search is already saved, return early to prevent unnecessary API calls
- Navigation.dismissModal();
+ // If the search is already saved, we only display the results as we don't need to save it.
+ applyFiltersAndNavigate();
return;
}
diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx
index f5687c09bd8d..7d9550c5ec06 100644
--- a/src/pages/Search/EmptySearchView.tsx
+++ b/src/pages/Search/EmptySearchView.tsx
@@ -12,6 +12,7 @@ import MenuItem from '@components/MenuItem';
import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
@@ -116,6 +117,9 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) {
});
const viewTourTaskReportID = introSelected?.viewTour;
const [viewTourTaskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${viewTourTaskReportID}`);
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const canModifyTask = Task.canModifyTask(viewTourTaskReport, currentUserPersonalDetails.accountID);
+ const canActionTask = Task.canActionTask(viewTourTaskReport, currentUserPersonalDetails.accountID);
const content = useMemo(() => {
switch (type) {
@@ -149,7 +153,9 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) {
buttonAction: () => {
Link.openExternalLink(navatticURL);
Welcome.setSelfTourViewed();
- Task.completeTask(viewTourTaskReport);
+ if (viewTourTaskReport && canModifyTask && canActionTask) {
+ Task.completeTask(viewTourTaskReport);
+ }
},
},
]
@@ -187,7 +193,9 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) {
buttonAction: () => {
Link.openExternalLink(navatticURL);
Welcome.setSelfTourViewed();
- Task.completeTask(viewTourTaskReport);
+ if (viewTourTaskReport && canModifyTask && canActionTask) {
+ Task.completeTask(viewTourTaskReport);
+ }
},
},
]
@@ -235,6 +243,8 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) {
shouldRedirectToExpensifyClassic,
hasResults,
viewTourTaskReport,
+ canModifyTask,
+ canActionTask,
]);
return (
diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx
index 110005c263f9..3fe047ab3187 100644
--- a/src/pages/Search/SearchTypeMenu.tsx
+++ b/src/pages/Search/SearchTypeMenu.tsx
@@ -21,7 +21,7 @@ import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
import * as SearchActions from '@libs/actions/Search';
import Navigation from '@libs/Navigation/Navigation';
-import {getAllTaxRates, hasWorkspaceWithInvoices} from '@libs/PolicyUtils';
+import {canSendInvoice, getAllTaxRates} from '@libs/PolicyUtils';
import {hasInvoiceReports} from '@libs/ReportUtils';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import * as SearchUIUtils from '@libs/SearchUIUtils';
@@ -71,7 +71,7 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
);
const {showDeleteModal, DeleteConfirmModal} = useDeleteSavedSearch();
const [session] = useOnyx(ONYXKEYS.SESSION);
-
+ const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const personalDetails = usePersonalDetails();
const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const taxRates = getAllTaxRates();
@@ -97,7 +97,7 @@ function SearchTypeMenu({queryJSON, searchName}: SearchTypeMenuProps) {
},
];
- if (hasWorkspaceWithInvoices(session?.email) || hasInvoiceReports()) {
+ if (canSendInvoice(allPolicies, session?.email) || hasInvoiceReports()) {
typeMenuItems.push({
title: translate('workspace.common.invoices'),
type: CONST.SEARCH.DATA_TYPES.INVOICE,
diff --git a/src/pages/Travel/TripDetailsPage.tsx b/src/pages/Travel/TripDetailsPage.tsx
index d7a93e7cdeba..6236fb249001 100644
--- a/src/pages/Travel/TripDetailsPage.tsx
+++ b/src/pages/Travel/TripDetailsPage.tsx
@@ -48,8 +48,8 @@ function TripDetailsPage({route}: TripDetailsPageProps) {
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${route.params.transactionID}`);
- const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID ?? '-1'}`);
- const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`);
+ const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID ?? CONST.DEFAULT_NUMBER_ID}`);
+ const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? CONST.DEFAULT_NUMBER_ID}`);
const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.reportID);
const reservationType = transaction?.receipt?.reservationList?.at(route.params.reservationIndex ?? 0)?.type;
@@ -67,7 +67,7 @@ function TripDetailsPage({route}: TripDetailsPageProps) {
>
{
if (!subtitle) {
return false;
@@ -142,7 +144,6 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto
const defaultSubscriptSize = ReportUtils.isExpenseRequest(report) ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT;
const icons = ReportUtils.getIcons(reportHeaderData, personalDetails, null, '', -1, policy, invoiceReceiverPolicy);
const brickRoadIndicator = ReportUtils.hasReportNameError(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '';
- const shouldShowBorderBottom = !isTaskReport || !shouldUseNarrowLayout;
const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(report);
const shouldUseGroupTitle = isGroupChat && (!!report?.reportName || !isMultipleParticipant);
const isLoading = !report?.reportID || !title;
@@ -154,7 +155,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto
return (
@@ -258,7 +259,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto
{
if (ReportUtils.canEditPolicyDescription(policy)) {
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(report.policyID ?? '-1'));
+ Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(report.policyID));
return;
}
Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, Navigation.getReportRHPActiveRoute()));
@@ -286,7 +287,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto
{!shouldUseNarrowLayout && isChatUsedForOnboarding && }
- {isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && }
+ {!shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && }
{!isParentReportLoading && canJoin && !shouldUseNarrowLayout && joinButton}
{shouldDisplaySearchRouter && }
@@ -315,6 +316,13 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto
addSpacing
/>
)}
+ {!!report && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && (
+
+
+
+
+
+ )}
);
}
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index ef3137a8c7d2..b02ccec1a56b 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -15,7 +15,6 @@ import MoneyRequestHeader from '@components/MoneyRequestHeader';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
import ScreenWrapper from '@components/ScreenWrapper';
-import TaskHeaderActionButton from '@components/TaskHeaderActionButton';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useAppFocusEvent from '@hooks/useAppFocusEvent';
import type {CurrentReportIDContextValue} from '@hooks/useCurrentReportID';
@@ -777,15 +776,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
needsOffscreenAlphaCompositing
>
{headerView}
- {!!report && ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && (
-
-
-
-
-
-
-
- )}
{!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && (
diff --git a/src/pages/home/report/comment/RenderCommentHTML.tsx b/src/pages/home/report/comment/RenderCommentHTML.tsx
index e730ae061519..fc5679f8a1f1 100644
--- a/src/pages/home/report/comment/RenderCommentHTML.tsx
+++ b/src/pages/home/report/comment/RenderCommentHTML.tsx
@@ -5,10 +5,12 @@ import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage';
type RenderCommentHTMLProps = {
source: OriginalMessageSource;
html: string;
+ containsOnlyEmojis: boolean;
};
-function RenderCommentHTML({html, source}: RenderCommentHTMLProps) {
- const commentHtml = source === 'email' ? `${html} ` : `${html} `;
+function RenderCommentHTML({html, source, containsOnlyEmojis}: RenderCommentHTMLProps) {
+ const commentHtml =
+ source === 'email' ? `${html} ` : `${html} `;
return ;
}
diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx
index 5dc8c6a85b85..f8ea9b56871f 100644
--- a/src/pages/home/report/comment/TextCommentFragment.tsx
+++ b/src/pages/home/report/comment/TextCommentFragment.tsx
@@ -68,13 +68,12 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so
const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text ?? '');
const containsEmojis = CONST.REGEX.ALL_EMOJIS.test(text ?? '');
if (!shouldRenderAsText(html, text ?? '') && !(containsOnlyEmojis && styleAsDeleted)) {
- const editedTag = fragment?.isEdited ? ` ` : '';
+ const editedTag = fragment?.isEdited ? ` ` : '';
const htmlWithDeletedTag = styleAsDeleted ? `${html}` : html;
let htmlContent = htmlWithDeletedTag;
if (containsOnlyEmojis) {
htmlContent = Str.replaceAll(htmlContent, '', '');
- htmlContent = Str.replaceAll(htmlContent, '', '');
} else if (containsEmojis) {
htmlContent = Str.replaceAll(htmlWithDeletedTag, '', '');
}
@@ -87,6 +86,7 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so
return (
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
index 3464fab8e72a..393f396ece60 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx
@@ -12,6 +12,7 @@ import * as Expensicons from '@components/Icon/Expensicons';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import PopoverMenu from '@components/PopoverMenu';
import {useProductTrainingContext} from '@components/ProductTrainingContext';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
@@ -447,6 +448,9 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
const viewTourTaskReportID = introSelected?.viewTour;
const [viewTourTaskReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${viewTourTaskReportID}`);
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const canModifyTask = Task.canModifyTask(viewTourTaskReport, currentUserPersonalDetails.accountID);
+ const canActionTask = Task.canActionTask(viewTourTaskReport, currentUserPersonalDetails.accountID);
return (
@@ -506,7 +510,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl
onSelected: () => {
Link.openExternalLink(navatticURL);
Welcome.setSelfTourViewed(Session.isAnonymousUser());
- if (viewTourTaskReport) {
+ if (viewTourTaskReport && canModifyTask && canActionTask) {
Task.completeTask(viewTourTaskReport);
}
},
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index 127885289fb1..990296d245fb 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -74,10 +74,10 @@ function IOURequestStepScan({
const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false);
const [fileResize, setFileResize] = useState(null);
const [fileSource, setFileSource] = useState('');
- const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID ?? -1}`);
+ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID ?? CONST.DEFAULT_NUMBER_ID}`);
const policy = usePolicy(report?.policyID);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
- const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? -1}`);
+ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`);
const platform = getPlatform(true);
const [mutedPlatforms = {}] = useOnyx(ONYXKEYS.NVP_MUTED_PLATFORMS);
const isPlatformMuted = mutedPlatforms[platform];
@@ -198,7 +198,10 @@ function IOURequestStepScan({
}
if (!Str.isImage(file.name ?? '') && (file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE) {
- Alert.alert(translate('attachmentPicker.attachmentTooLarge'), translate('attachmentPicker.sizeExceeded'));
+ Alert.alert(
+ translate('attachmentPicker.attachmentTooLarge'),
+ translate('attachmentPicker.sizeExceededWithLimit', {maxUploadSizeInMB: CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE / (1024 * 1024)}),
+ );
return false;
}
@@ -295,7 +298,7 @@ function IOURequestStepScan({
// be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step.
const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report);
const participants = selectedParticipants.map((participant) => {
- const participantAccountID = participant?.accountID ?? -1;
+ const participantAccountID = participant?.accountID ?? CONST.DEFAULT_NUMBER_ID;
return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant);
});
@@ -308,10 +311,10 @@ function IOURequestStepScan({
IOU.startSplitBill({
participants,
currentUserLogin: currentUserPersonalDetails?.login ?? '',
- currentUserAccountID: currentUserPersonalDetails?.accountID ?? -1,
+ currentUserAccountID: currentUserPersonalDetails.accountID,
comment: '',
receipt,
- existingSplitChatReportID: reportID ?? -1,
+ existingSplitChatReportID: reportID,
billable: false,
category: '',
tag: '',
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
index 382da92fe94d..227076b9f4f3 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
@@ -86,10 +86,10 @@ function IOURequestStepScan({
const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false);
const getScreenshotTimeoutRef = useRef(null);
- const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID ?? -1}`);
+ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID ?? CONST.DEFAULT_NUMBER_ID}`);
const policy = usePolicy(report?.policyID);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
- const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? -1}`);
+ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`);
const [isLoadingReceipt, setIsLoadingReceipt] = useState(false);
const [videoConstraints, setVideoConstraints] = useState();
@@ -294,7 +294,7 @@ function IOURequestStepScan({
// be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step.
const selectedParticipants = IOU.setMoneyRequestParticipantsFromReport(transactionID, report);
const participants = selectedParticipants.map((participant) => {
- const participantAccountID = participant?.accountID ?? -1;
+ const participantAccountID = participant?.accountID ?? CONST.DEFAULT_NUMBER_ID;
return participantAccountID ? OptionsListUtils.getParticipantsOption(participant, personalDetails) : OptionsListUtils.getReportOption(participant);
});
@@ -307,10 +307,10 @@ function IOURequestStepScan({
IOU.startSplitBill({
participants,
currentUserLogin: currentUserPersonalDetails?.login ?? '',
- currentUserAccountID: currentUserPersonalDetails?.accountID ?? -1,
+ currentUserAccountID: currentUserPersonalDetails.accountID,
comment: '',
receipt,
- existingSplitChatReportID: reportID ?? -1,
+ existingSplitChatReportID: reportID,
billable: false,
category: '',
tag: '',
@@ -591,6 +591,16 @@ function IOURequestStepScan({
/>
) : null;
+ const getConfirmModalPrompt = () => {
+ if (!attachmentInvalidReason) {
+ return '';
+ }
+ if (attachmentInvalidReason === 'attachmentPicker.sizeExceededWithLimit') {
+ return translate(attachmentInvalidReason, {maxUploadSizeInMB: CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE / (1024 * 1024)});
+ }
+ return translate(attachmentInvalidReason);
+ };
+
const mobileCameraView = () => (
<>
@@ -767,7 +777,7 @@ function IOURequestStepScan({
onConfirm={hideRecieptModal}
onCancel={hideRecieptModal}
isVisible={isAttachmentInvalid}
- prompt={attachmentInvalidReason ? translate(attachmentInvalidReason) : ''}
+ prompt={getConfirmModalPrompt()}
confirmText={translate('common.close')}
shouldShowCancelButton={false}
/>
diff --git a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx
index 1f2b80aa5f4e..15704221f259 100644
--- a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx
+++ b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx
@@ -56,7 +56,7 @@ function PhoneNumberPage() {
errors[INPUT_IDS.PHONE_NUMBER] = translate('common.error.fieldRequired');
}
const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(values[INPUT_IDS.PHONE_NUMBER]);
- const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode);
+ const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(values[INPUT_IDS.PHONE_NUMBER]);
if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) {
errors[INPUT_IDS.PHONE_NUMBER] = translate('bankAccount.error.phoneNumber');
}
diff --git a/src/pages/settings/Subscription/SubscriptionPlan.tsx b/src/pages/settings/Subscription/SubscriptionPlan.tsx
deleted file mode 100644
index 33933027dd45..000000000000
--- a/src/pages/settings/Subscription/SubscriptionPlan.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react';
-import {View} from 'react-native';
-import {useOnyx} from 'react-native-onyx';
-import Icon from '@components/Icon';
-import * as Expensicons from '@components/Icon/Expensicons';
-import * as Illustrations from '@components/Icon/Illustrations';
-import Section from '@components/Section';
-import Text from '@components/Text';
-import useLocalize from '@hooks/useLocalize';
-import usePreferredCurrency from '@hooks/usePreferredCurrency';
-import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
-import useSubscriptionPrice from '@hooks/useSubscriptionPrice';
-import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
-import {convertToShortDisplayString} from '@libs/CurrencyUtils';
-import variables from '@styles/variables';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import SaveWithExpensifyButton from './SaveWithExpensifyButton';
-
-function SubscriptionPlan() {
- const {translate} = useLocalize();
- const styles = useThemeStyles();
- const theme = useTheme();
-
- const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);
-
- const subscriptionPlan = useSubscriptionPlan();
- const subscriptionPrice = useSubscriptionPrice();
- const preferredCurrency = usePreferredCurrency();
-
- const isCollect = subscriptionPlan === CONST.POLICY.TYPE.TEAM;
- const isAnnual = privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL;
-
- const benefitsList = isCollect
- ? [
- translate('subscription.yourPlan.collect.benefit1'),
- translate('subscription.yourPlan.collect.benefit2'),
- translate('subscription.yourPlan.collect.benefit3'),
- translate('subscription.yourPlan.collect.benefit4'),
- translate('subscription.yourPlan.collect.benefit5'),
- translate('subscription.yourPlan.collect.benefit6'),
- translate('subscription.yourPlan.collect.benefit7'),
- ]
- : [
- translate('subscription.yourPlan.control.benefit1'),
- translate('subscription.yourPlan.control.benefit2'),
- translate('subscription.yourPlan.control.benefit3'),
- translate('subscription.yourPlan.control.benefit4'),
- translate('subscription.yourPlan.control.benefit5'),
- translate('subscription.yourPlan.control.benefit6'),
- ];
-
- return (
-
-
-
- {translate(`subscription.yourPlan.${isCollect ? 'collect' : 'control'}.title`)}
-
- {translate(`subscription.yourPlan.${isCollect ? 'collect' : 'control'}.${isAnnual ? 'priceAnnual' : 'pricePayPerUse'}`, {
- lower: convertToShortDisplayString(subscriptionPrice, preferredCurrency),
- upper: convertToShortDisplayString(subscriptionPrice * CONST.SUBSCRIPTION_PRICE_FACTOR, preferredCurrency),
- })}
-
- {benefitsList.map((benefit) => (
-
-
- {benefit}
-
- ))}
-
-
-
-
- {translate('subscription.yourPlan.saveWithExpensifyTitle')}
- {translate('subscription.yourPlan.saveWithExpensifyDescription')}
-
-
-
-
- );
-}
-
-SubscriptionPlan.displayName = 'SubscriptionPlan';
-
-export default SubscriptionPlan;
diff --git a/src/pages/settings/Subscription/SaveWithExpensifyButton/index.native.tsx b/src/pages/settings/Subscription/SubscriptionPlan/SaveWithExpensifyButton/index.native.tsx
similarity index 100%
rename from src/pages/settings/Subscription/SaveWithExpensifyButton/index.native.tsx
rename to src/pages/settings/Subscription/SubscriptionPlan/SaveWithExpensifyButton/index.native.tsx
diff --git a/src/pages/settings/Subscription/SaveWithExpensifyButton/index.tsx b/src/pages/settings/Subscription/SubscriptionPlan/SaveWithExpensifyButton/index.tsx
similarity index 100%
rename from src/pages/settings/Subscription/SaveWithExpensifyButton/index.tsx
rename to src/pages/settings/Subscription/SubscriptionPlan/SaveWithExpensifyButton/index.tsx
diff --git a/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCard.tsx b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCard.tsx
new file mode 100644
index 000000000000..33d3f40a0b36
--- /dev/null
+++ b/src/pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCard.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import {View} from 'react-native';
+import type {SvgProps} from 'react-native-svg';
+import type {ValueOf} from 'type-fest';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import {PressableWithFeedback} from '@components/Pressable';
+import SelectCircle from '@components/SelectCircle';
+import Text from '@components/Text';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import type CONST from '@src/CONST';
+
+type PersonalPolicyTypeExludedProps = Exclude, 'personal'>;
+
+type SubscriptionPlanCardProps = {
+ index: number;
+ plan: {
+ title: string;
+ src: React.FC;
+ benefits: string[];
+ description: string;
+ isSelected: boolean;
+ type: PersonalPolicyTypeExludedProps;
+ };
+
+ onPress: (type: PersonalPolicyTypeExludedProps) => void;
+};
+function SubscriptionPlanCard({plan, index, onPress}: SubscriptionPlanCardProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+
+ return (
+
+ onPress(plan.type)}
+ >
+
+
+
+
+
+
+ {plan.title}
+ {plan.description}
+ {plan.benefits.map((benefit) => (
+
+
+ {benefit}
+
+ ))}
+
+
+ );
+}
+
+SubscriptionPlanCard.displayName = 'SubscriptionPlanCard';
+
+export default SubscriptionPlanCard;
+export type {PersonalPolicyTypeExludedProps};
diff --git a/src/pages/settings/Subscription/SubscriptionPlan/index.tsx b/src/pages/settings/Subscription/SubscriptionPlan/index.tsx
new file mode 100644
index 000000000000..da8caab785f1
--- /dev/null
+++ b/src/pages/settings/Subscription/SubscriptionPlan/index.tsx
@@ -0,0 +1,151 @@
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import Icon from '@components/Icon';
+import * as Illustrations from '@components/Icon/Illustrations';
+import Section from '@components/Section';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import usePreferredCurrency from '@hooks/usePreferredCurrency';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useSubscriptionPlan from '@hooks/useSubscriptionPlan';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getCurrentUserAccountID} from '@libs/actions/Report';
+import {convertToShortDisplayString} from '@libs/CurrencyUtils';
+import {getOwnedPaidPolicies} from '@libs/PolicyUtils';
+import Navigation from '@navigation/Navigation';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import SaveWithExpensifyButton from './SaveWithExpensifyButton';
+import SubscriptionPlanCard from './SubscriptionPlanCard';
+import type {PersonalPolicyTypeExludedProps} from './SubscriptionPlanCard';
+
+function SubscriptionPlan() {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const currentUserAccountID = getCurrentUserAccountID();
+ const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
+ const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);
+ const subscriptionPlan = useSubscriptionPlan();
+ const ownerPolicies = useMemo(() => getOwnedPaidPolicies(policies, currentUserAccountID), [policies, currentUserAccountID]);
+ const preferredCurrency = usePreferredCurrency();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ const isAnnual = privateSubscription?.type === CONST.SUBSCRIPTION.TYPE.ANNUAL;
+
+ function getSubscriptionPrice(plan: PersonalPolicyTypeExludedProps): number {
+ if (!privateSubscription?.type) {
+ return 0;
+ }
+
+ return CONST.SUBSCRIPTION_PRICES[preferredCurrency][plan][privateSubscription.type];
+ }
+
+ const plans = [
+ {
+ type: CONST.POLICY.TYPE.TEAM,
+ title: translate('subscription.yourPlan.collect.title'),
+ benefits: [
+ translate('subscription.yourPlan.collect.benefit1'),
+ translate('subscription.yourPlan.collect.benefit2'),
+ translate('subscription.yourPlan.collect.benefit3'),
+ translate('subscription.yourPlan.collect.benefit4'),
+ translate('subscription.yourPlan.collect.benefit5'),
+ translate('subscription.yourPlan.collect.benefit6'),
+ translate('subscription.yourPlan.collect.benefit7'),
+ ],
+ src: Illustrations.Mailbox,
+ description: translate(`subscription.yourPlan.collect.${isAnnual ? 'priceAnnual' : 'pricePayPerUse'}`, {
+ lower: convertToShortDisplayString(getSubscriptionPrice(CONST.POLICY.TYPE.TEAM), preferredCurrency),
+ upper: convertToShortDisplayString(getSubscriptionPrice(CONST.POLICY.TYPE.TEAM) * CONST.SUBSCRIPTION_PRICE_FACTOR, preferredCurrency),
+ }),
+ isSelected: subscriptionPlan === CONST.POLICY.TYPE.TEAM,
+ },
+ {
+ type: CONST.POLICY.TYPE.CORPORATE,
+ title: translate('subscription.yourPlan.control.title'),
+ benefits: [
+ translate('subscription.yourPlan.control.benefit1'),
+ translate('subscription.yourPlan.control.benefit2'),
+ translate('subscription.yourPlan.control.benefit3'),
+ translate('subscription.yourPlan.control.benefit4'),
+ translate('subscription.yourPlan.control.benefit5'),
+ translate('subscription.yourPlan.control.benefit6'),
+ ],
+ src: Illustrations.ShieldYellow,
+ description: translate(`subscription.yourPlan.control.${isAnnual ? 'priceAnnual' : 'pricePayPerUse'}`, {
+ lower: convertToShortDisplayString(getSubscriptionPrice(CONST.POLICY.TYPE.CORPORATE), preferredCurrency),
+ upper: convertToShortDisplayString(getSubscriptionPrice(CONST.POLICY.TYPE.CORPORATE) * CONST.SUBSCRIPTION_PRICE_FACTOR, preferredCurrency),
+ }),
+ isSelected: subscriptionPlan === CONST.POLICY.TYPE.CORPORATE,
+ },
+ ];
+
+ const handlePlanPress = (planType: PersonalPolicyTypeExludedProps) => {
+ // If the selected plan and the current plan are the same, and the user has no policies, return.
+ if (planType === subscriptionPlan || !ownerPolicies.length) {
+ return;
+ }
+
+ // If the user has one policy as owner and selected plan is team, navigate to downgrade page.
+ if (ownerPolicies.length === 1 && planType === CONST.POLICY.TYPE.TEAM) {
+ Navigation.navigate(ROUTES.WORKSPACE_DOWNGRADE.getRoute(ownerPolicies.at(0)?.id));
+ return;
+ }
+
+ // If the user has one policy as owner and selected plan is corporate, navigate to upgrade page.
+ if (ownerPolicies.length === 1 && planType === CONST.POLICY.TYPE.CORPORATE) {
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(ownerPolicies.at(0)?.id));
+ return;
+ }
+
+ // If the user has multiple policies as owner and selected plan is team, navigate to downgrade page.
+ if (ownerPolicies.length > 1 && planType === CONST.POLICY.TYPE.TEAM) {
+ Navigation.navigate(ROUTES.WORKSPACE_DOWNGRADE.getRoute());
+ return;
+ }
+
+ // If the user has multiple policies as owner and selected plan is corporate, navigate to upgrade page.
+ if (ownerPolicies.length > 1 && planType === CONST.POLICY.TYPE.CORPORATE) {
+ Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute());
+ }
+ };
+
+ return (
+
+
+ {plans.map((plan, index) => (
+
+ ))}
+
+
+
+
+ {translate('subscription.yourPlan.saveWithExpensifyTitle')}
+ {translate('subscription.yourPlan.saveWithExpensifyDescription')}
+
+
+
+
+ );
+}
+
+SubscriptionPlan.displayName = 'SubscriptionPlan';
+
+export default SubscriptionPlan;
diff --git a/src/pages/workspace/WorkspaceProfilePlanTypePage.tsx b/src/pages/workspace/WorkspaceProfilePlanTypePage.tsx
index 253f64d1574f..c271aa4d518b 100644
--- a/src/pages/workspace/WorkspaceProfilePlanTypePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePlanTypePage.tsx
@@ -20,6 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import OpenWorkspacePlanPage from '@libs/actions/Policy/Plan';
import Navigation from '@navigation/Navigation';
import CardSectionUtils from '@pages/settings/Subscription/CardSection/utils';
+import type {PersonalPolicyTypeExludedProps} from '@pages/settings/Subscription/SubscriptionPlan/SubscriptionPlanCard';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -57,8 +58,8 @@ function WorkspaceProfilePlanTypePage({policy}: WithPolicyProps) {
.filter((type) => type !== CONST.POLICY.TYPE.PERSONAL)
.map((policyType) => ({
value: policyType,
- text: translate(`workspace.planTypePage.planTypes.${policyType as Exclude}.label`),
- alternateText: translate(`workspace.planTypePage.planTypes.${policyType as Exclude}.description`),
+ text: translate(`workspace.planTypePage.planTypes.${policyType as PersonalPolicyTypeExludedProps}.label`),
+ alternateText: translate(`workspace.planTypePage.planTypes.${policyType as PersonalPolicyTypeExludedProps}.description`),
keyForList: policyType,
isSelected: policyType === currentPlan,
}))
diff --git a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx
index b60401129635..119786e3b28e 100644
--- a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx
+++ b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx
@@ -45,7 +45,7 @@ function CategoryRequireReceiptsOverPage({
const isAlwaysSelected = policyCategories?.[categoryName]?.maxAmountNoReceipt === 0;
const isNeverSelected = policyCategories?.[categoryName]?.maxAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE;
- const maxExpenseAmountToDisplay = policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE ? 0 : policy?.maxExpenseAmount;
+ const maxExpenseAmountToDisplay = policy?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE ? 0 : policy?.maxExpenseAmountNoReceipt;
const requireReceiptsOverListData = [
{
diff --git a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx
index 2c6745fabe14..15ead5b9a323 100644
--- a/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx
+++ b/src/pages/workspace/companyCards/addNew/SelectBankStep.tsx
@@ -94,6 +94,7 @@ function SelectBankStep() {
showConfirmButton
confirmButtonText={translate('common.next')}
onConfirm={submit}
+ confirmButtonStyles={styles.mt5}
>
{hasError && (
diff --git a/src/pages/workspace/downgrade/DowngradeIntro.tsx b/src/pages/workspace/downgrade/DowngradeIntro.tsx
index ba8c91550561..d226576d84cb 100644
--- a/src/pages/workspace/downgrade/DowngradeIntro.tsx
+++ b/src/pages/workspace/downgrade/DowngradeIntro.tsx
@@ -10,15 +10,18 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {openLink} from '@libs/actions/Link';
+import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
type Props = {
buttonDisabled?: boolean;
loading?: boolean;
onDowngrade: () => void;
+ policyID?: string;
};
-function DowngradeIntro({onDowngrade, buttonDisabled, loading}: Props) {
+function DowngradeIntro({onDowngrade, buttonDisabled, loading, policyID}: Props) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {environmentURL} = useEnvironment();
@@ -67,14 +70,23 @@ function DowngradeIntro({onDowngrade, buttonDisabled, loading}: Props) {
{translate('workspace.downgrade.commonFeatures.benefits.warning')}
-
+ {policyID ? (
+
+ ) : (
+ Navigation.navigate(ROUTES.SETTINGS_WORKSPACES, CONST.NAVIGATION.TYPE.UP)}
+ large
+ />
+ )}
);
}
diff --git a/src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx b/src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx
index ab9a0c9fbfde..2d18693dc24b 100644
--- a/src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx
+++ b/src/pages/workspace/downgrade/WorkspaceDowngradePage.tsx
@@ -20,17 +20,16 @@ type WorkspaceDowngradePageProps = PlatformStackScreenProps PolicyUtils.canModifyPlan(policyID), [policyID]);
const isDowngraded = useMemo(() => PolicyUtils.isCollectPolicy(policy), [policy]);
const downgradeToTeam = () => {
- if (!canPerformDowngrade) {
+ if (!canPerformDowngrade || !policy) {
return;
}
Policy.downgradeToTeam(policy.id);
@@ -56,7 +55,7 @@ function WorkspaceDowngradePage({route}: WorkspaceDowngradePageProps) {
}
}}
/>
- {isDowngraded && (
+ {isDowngraded && !!policyID && (
{
Navigation.dismissModal();
@@ -66,6 +65,7 @@ function WorkspaceDowngradePage({route}: WorkspaceDowngradePageProps) {
)}
{!isDowngraded && (
void;
+ policyID?: string;
};
-function GenericFeaturesView({onUpgrade, buttonDisabled, loading}: GenericFeaturesViewProps) {
+function GenericFeaturesView({onUpgrade, buttonDisabled, loading, policyID}: GenericFeaturesViewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isExtraSmallScreenWidth} = useResponsiveLayout();
+ const preferredCurrency = usePreferredCurrency();
const benefits = [
translate('workspace.upgrade.commonFeatures.benefits.benefit1'),
@@ -29,6 +34,12 @@ function GenericFeaturesView({onUpgrade, buttonDisabled, loading}: GenericFeatur
translate('workspace.upgrade.commonFeatures.benefits.benefit4'),
];
+ const formattedPrice = React.useMemo(() => {
+ const upgradeCurrency = Object.hasOwn(CONST.SUBSCRIPTION_PRICES, preferredCurrency) ? preferredCurrency : CONST.PAYMENT_CARD_CURRENCY.USD;
+ const upgradePrice = CONST.SUBSCRIPTION_PRICES[upgradeCurrency][CONST.POLICY.TYPE.CORPORATE][CONST.SUBSCRIPTION.TYPE.ANNUAL];
+ return `${convertToShortDisplayString(upgradePrice, upgradeCurrency)} `;
+ }, [preferredCurrency]);
+
return (
@@ -51,7 +62,9 @@ function GenericFeaturesView({onUpgrade, buttonDisabled, loading}: GenericFeatur
))}
- {translate('workspace.upgrade.commonFeatures.benefits.note')}{' '}
+ {translate('workspace.upgrade.commonFeatures.benefits.startsAt')}
+ {formattedPrice}
+ {translate('workspace.upgrade.commonFeatures.benefits.perMember')}{' '}
Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION)}
@@ -61,14 +74,23 @@ function GenericFeaturesView({onUpgrade, buttonDisabled, loading}: GenericFeatur
{translate('workspace.upgrade.commonFeatures.benefits.pricing')}
-
+ {policyID ? (
+
+ ) : (
+ Navigation.navigate(ROUTES.SETTINGS_WORKSPACES, CONST.NAVIGATION.TYPE.UP)}
+ large
+ />
+ )}
);
}
diff --git a/src/pages/workspace/upgrade/UpgradeIntro.tsx b/src/pages/workspace/upgrade/UpgradeIntro.tsx
index a13519c87ad0..9d6ae871826c 100644
--- a/src/pages/workspace/upgrade/UpgradeIntro.tsx
+++ b/src/pages/workspace/upgrade/UpgradeIntro.tsx
@@ -28,9 +28,10 @@ type Props = {
feature?: ValueOf;
onUpgrade: () => void;
isCategorizing?: boolean;
+ policyID?: string;
};
-function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing}: Props) {
+function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, policyID}: Props) {
const styles = useThemeStyles();
const {isExtraSmallScreenWidth} = useResponsiveLayout();
const {translate} = useLocalize();
@@ -45,12 +46,13 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi
return `${convertToShortDisplayString(upgradePrice, upgradeCurrency)} `;
}, [preferredCurrency, isCategorizing]);
- if (!feature) {
+ if (!feature || !policyID) {
return (
);
}
diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
index 26d2509f0f1f..e31ef3232ba4 100644
--- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
+++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
@@ -39,9 +39,9 @@ function getFeatureNameAlias(featureName: string) {
function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
const styles = useThemeStyles();
- const policyID = route.params.policyID;
+ const policyID = route.params?.policyID;
- const featureNameAlias = route.params.featureName && getFeatureNameAlias(route.params.featureName);
+ const featureNameAlias = route.params?.featureName && getFeatureNameAlias(route.params.featureName);
const feature = useMemo(() => Object.values(CONST.UPGRADE_FEATURE_INTRO_MAPPING).find((f) => f.alias === featureNameAlias), [featureNameAlias]);
const {translate} = useLocalize();
@@ -49,14 +49,14 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
const qboConfig = policy?.connections?.quickbooksOnline?.config;
const {isOffline} = useNetwork();
- const canPerformUpgrade = !!policy && PolicyUtils.isPolicyAdmin(policy);
+ const canPerformUpgrade = useMemo(() => PolicyUtils.canModifyPlan(policyID), [policyID]);
const isUpgraded = useMemo(() => PolicyUtils.isControlPolicy(policy), [policy]);
const perDiemCustomUnit = PolicyUtils.getPerDiemCustomUnit(policy);
const categoryId = route.params?.categoryId;
const goBack = useCallback(() => {
- if (!feature) {
+ if (!feature || !policyID) {
Navigation.dismissModal();
return;
}
@@ -89,10 +89,10 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
Navigation.dismissModal();
return route.params.backTo ? Navigation.navigate(route.params.backTo) : Navigation.goBack();
}
- }, [feature, policyID, route.params.backTo, route.params.featureName]);
+ }, [feature, policyID, route.params?.backTo, route.params?.featureName]);
const upgradeToCorporate = () => {
- if (!canPerformUpgrade) {
+ if (!canPerformUpgrade || !policy) {
return;
}
@@ -100,7 +100,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
};
const confirmUpgrade = useCallback(() => {
- if (!feature) {
+ if (!feature || !policyID) {
return;
}
switch (feature.id) {
@@ -153,7 +153,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
qboConfig?.syncClasses,
qboConfig?.syncCustomers,
qboConfig?.syncLocations,
- route.params.featureName,
+ route.params?.featureName,
]);
useFocusEffect(
@@ -187,7 +187,7 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) {
}
}}
/>
- {isUpgraded && (
+ {!!policy && isUpgraded && (
)}
diff --git a/src/styles/utils/getSafeAreaInsets/index.android.ts b/src/styles/utils/getSafeAreaInsets/index.android.ts
index c77ef32f7370..53ff17567f6c 100644
--- a/src/styles/utils/getSafeAreaInsets/index.android.ts
+++ b/src/styles/utils/getSafeAreaInsets/index.android.ts
@@ -1,5 +1,4 @@
import type {EdgeInsets} from 'react-native-safe-area-context';
-import StatusBar from '@libs/StatusBar';
import defaultInsets from './defaultInsets';
/**
@@ -9,10 +8,7 @@ import defaultInsets from './defaultInsets';
function getSafeAreaInsets(safeAreaInsets: EdgeInsets | null): EdgeInsets {
const insets = safeAreaInsets ?? defaultInsets;
- return {
- ...insets,
- top: StatusBar.currentHeight ?? insets.top,
- };
+ return insets;
}
export default getSafeAreaInsets;
diff --git a/src/types/onyx/NewGroupChatDraft.ts b/src/types/onyx/NewGroupChatDraft.ts
index e075ed6e5e33..1ca29d621867 100644
--- a/src/types/onyx/NewGroupChatDraft.ts
+++ b/src/types/onyx/NewGroupChatDraft.ts
@@ -4,7 +4,7 @@ type SelectedParticipant = {
accountID: number;
/** Participant login name */
- login: string;
+ login: string | undefined;
};
/** Model of new group chat draft */
diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts
index dc07c16c8d7f..1de832fb5eea 100644
--- a/tests/actions/IOUTest.ts
+++ b/tests/actions/IOUTest.ts
@@ -2,9 +2,12 @@ import {format} from 'date-fns';
import isEqual from 'lodash/isEqual';
import type {OnyxCollection, OnyxEntry, OnyxInputValue} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
+import {WRITE_COMMANDS} from '@libs/API/types';
+import type {ApiCommand} from '@libs/API/types';
import type {OptimisticChatReport} from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
+import type {IOUAction} from '@src/CONST';
import * as IOU from '@src/libs/actions/IOU';
import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
import * as PolicyActions from '@src/libs/actions/Policy/Policy';
@@ -43,6 +46,7 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({
dismissModal: jest.fn(),
dismissModalWithReport: jest.fn(),
goBack: jest.fn(),
+ setNavigationActionToMicrotaskQueue: jest.fn(),
}));
jest.mock('@src/libs/Navigation/isSearchTopmostCentralPane', () => jest.fn());
@@ -4272,4 +4276,114 @@ describe('actions/IOU', () => {
});
});
});
+
+ describe('should have valid parameters', () => {
+ let writeSpy: jest.SpyInstance;
+ const isValid = (value: unknown) => !value || typeof value !== 'object' || value instanceof Blob;
+
+ beforeEach(() => {
+ // eslint-disable-next-line rulesdir/no-multiple-api-calls
+ writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn());
+ });
+
+ afterEach(() => {
+ writeSpy.mockRestore();
+ });
+
+ test.each([
+ [WRITE_COMMANDS.REQUEST_MONEY, CONST.IOU.ACTION.CREATE],
+ [WRITE_COMMANDS.CONVERT_TRACKED_EXPENSE_TO_REQUEST, CONST.IOU.ACTION.SUBMIT],
+ ])('%s', async (expectedCommand: ApiCommand, action: IOUAction) => {
+ // When an expense is created
+ IOU.requestMoney({
+ action,
+ report: {reportID: ''},
+ participantParams: {
+ payeeEmail: RORY_EMAIL,
+ payeeAccountID: RORY_ACCOUNT_ID,
+ participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID},
+ },
+ transactionParams: {
+ amount: 10000,
+ attendees: [],
+ currency: CONST.CURRENCY.USD,
+ created: '',
+ merchant: 'KFC',
+ comment: '',
+ linkedTrackedExpenseReportAction: {
+ reportActionID: '',
+ actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ created: '2024-10-30',
+ },
+ actionableWhisperReportActionID: '1',
+ linkedTrackedExpenseReportID: '1',
+ },
+ });
+
+ await waitForBatchedUpdates();
+
+ // Then the correct API request should be made
+ expect(writeSpy).toHaveBeenCalledTimes(1);
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const [command, params] = writeSpy.mock.calls.at(0);
+ expect(command).toBe(expectedCommand);
+
+ // And the parameters should be supported by XMLHttpRequest
+ Object.values(params as Record).forEach((value) => {
+ expect(Array.isArray(value) ? value.every(isValid) : isValid(value)).toBe(true);
+ });
+ });
+
+ test.each([
+ [WRITE_COMMANDS.TRACK_EXPENSE, CONST.IOU.ACTION.CREATE],
+ [WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE, CONST.IOU.ACTION.CATEGORIZE],
+ [WRITE_COMMANDS.SHARE_TRACKED_EXPENSE, CONST.IOU.ACTION.SHARE],
+ ])('%s', async (expectedCommand: ApiCommand, action: IOUAction) => {
+ // When a track expense is created
+ IOU.trackExpense(
+ {reportID: ''},
+ 10000,
+ CONST.CURRENCY.USD,
+ '2024-10-30',
+ 'KFC',
+ RORY_EMAIL,
+ RORY_ACCOUNT_ID,
+ {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID},
+ '',
+ false,
+ {},
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ action,
+ '1',
+ {
+ reportActionID: '',
+ actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+ created: '2024-10-30',
+ },
+ '1',
+ );
+
+ await waitForBatchedUpdates();
+
+ // Then the correct API request should be made
+ expect(writeSpy).toHaveBeenCalledTimes(1);
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const [command, params] = writeSpy.mock.calls.at(0);
+ expect(command).toBe(expectedCommand);
+
+ // And the parameters should be supported by XMLHttpRequest
+ Object.values(params as Record).forEach((value) => {
+ expect(Array.isArray(value) ? value.every(isValid) : isValid(value)).toBe(true);
+ });
+ });
+ });
});
diff --git a/tests/unit/PhoneNumberTest.ts b/tests/unit/PhoneNumberTest.ts
index f720dc6a88e1..562f7343f812 100644
--- a/tests/unit/PhoneNumberTest.ts
+++ b/tests/unit/PhoneNumberTest.ts
@@ -31,7 +31,7 @@ describe('PhoneNumber', () => {
});
});
it('Should return invalid phone number', () => {
- const invalidNumbers = ['+165025300001', 'John Doe', '123', 'email@domain.com'];
+ const invalidNumbers = ['+165025300001', 'John Doe', '123', '0945789083', 'email@domain.com'];
invalidNumbers.forEach((givenPhone) => {
const parsedPhone = parsePhoneNumber(givenPhone);
diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts
index 259a470432b7..6f7828f8312b 100644
--- a/tests/unit/ReportUtilsTest.ts
+++ b/tests/unit/ReportUtilsTest.ts
@@ -1629,4 +1629,17 @@ describe('ReportUtils', () => {
expect(invoiceChatReport).toBeUndefined();
});
});
+ describe('getWorkspaceNameUpdatedMessage', () => {
+ it('return the encoded workspace name updated message', () => {
+ const action = {
+ originalMessage: {
+ newName: 'hello',
+ oldName: 'workspace 1',
+ },
+ };
+ expect(ReportUtils.getWorkspaceNameUpdatedMessage(action as ReportAction)).toEqual(
+ 'updated the name of this workspace to "hello" (previously "workspace 1")',
+ );
+ });
+ });
});
diff --git a/tests/unit/SearchAutocompleteParserTest.ts b/tests/unit/SearchAutocompleteParserTest.ts
index 9e8fd6e872ad..995d23eaeff5 100644
--- a/tests/unit/SearchAutocompleteParserTest.ts
+++ b/tests/unit/SearchAutocompleteParserTest.ts
@@ -58,6 +58,22 @@ const tests = [
],
},
},
+ {
+ query: parserCommonTests.quotesIOS,
+ expected: {
+ autocomplete: {
+ key: 'category',
+ length: 5,
+ start: 33,
+ value: 'a b',
+ },
+ ranges: [
+ {key: 'type', value: 'expense', start: 5, length: 7},
+ {key: 'status', value: 'all', start: 20, length: 3},
+ {key: 'category', value: 'a b', start: 33, length: 5},
+ ],
+ },
+ },
{
query: 'date>2024-01-01 amount>100 merchant:"A B" description:A,B,C ,, reportid:123456789 word',
expected: {
diff --git a/tests/unit/SearchParserTest.ts b/tests/unit/SearchParserTest.ts
index 9574be4aa2e1..e6c02a0ec80d 100644
--- a/tests/unit/SearchParserTest.ts
+++ b/tests/unit/SearchParserTest.ts
@@ -135,6 +135,20 @@ const tests = [
},
},
},
+ {
+ query: parserCommonTests.quotesIOS,
+ expected: {
+ type: 'expense',
+ status: 'all',
+ sortBy: 'date',
+ sortOrder: 'desc',
+ filters: {
+ operator: 'eq',
+ left: 'category',
+ right: 'a b',
+ },
+ },
+ },
{
query: ',',
expected: {
diff --git a/tests/utils/fixtures/searchParsersCommonQueries.ts b/tests/utils/fixtures/searchParsersCommonQueries.ts
index 1f398717d994..03c88ef995a3 100644
--- a/tests/utils/fixtures/searchParsersCommonQueries.ts
+++ b/tests/utils/fixtures/searchParsersCommonQueries.ts
@@ -6,6 +6,7 @@ const parserCommonTests = {
userFriendlyNames: 'tax-rate:rate1 expense-type:card card:"Big Bank" reportid:report',
oldNames: 'taxRate:rate1 expenseType:card cardID:"Big Bank" reportID:report',
complex: 'amount>200 expense-type:cash,card description:"Las Vegas party" date:2024-06-01 category:travel,hotel,"meal & entertainment"',
+ quotesIOS: 'type:expense status:all category:“a b”',
};
export default parserCommonTests;