diff --git a/android/app/build.gradle b/android/app/build.gradle index aaa6aaeb7a78..63aa4215cd90 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -96,8 +96,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042200 - versionName "1.4.22-0" + versionCode 1001042203 + versionName "1.4.22-3" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ecf3b8c9cad9..f58687c66c63 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.22.0 + 1.4.22.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 1ec1aeb6ce14..b7b8c9d3416b 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.22.0 + 1.4.22.3 diff --git a/package-lock.json b/package-lock.json index 2c1da1670e19..55bfafbec2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.22-0", + "version": "1.4.22-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.22-0", + "version": "1.4.22-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 118f63e45879..7264cb5fa25e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.22-0", + "version": "1.4.22-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index 2fd592f539c2..6e9118486a7a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1365,6 +1365,7 @@ const CONST = { DIGITS_AND_PLUS: /^\+?[0-9]*$/, ALPHABETIC_AND_LATIN_CHARS: /^[\p{Script=Latin} ]*$/u, NON_ALPHABETIC_AND_NON_LATIN_CHARS: /[^\p{Script=Latin}]/gu, + ACCENT_LATIN_CHARS: /[\u00C0-\u017F]/g, POSITIVE_INTEGER: /^\d+$/, PO_BOX: /\b[P|p]?(OST|ost)?\.?\s*[O|o|0]?(ffice|FFICE)?\.?\s*[B|b][O|o|0]?[X|x]?\.?\s+[#]?(\d+)\b/, ANY_VALUE: /^.+$/, @@ -1422,6 +1423,7 @@ const CONST = { ROUTES: { VALIDATE_LOGIN: /\/v($|(\/\/*))/, UNLINK_LOGIN: /\/u($|(\/\/*))/, + REDUNDANT_SLASHES: /(\/{2,})|(\/$)/g, }, TIME_STARTS_01: /^01:\d{2} [AP]M$/, @@ -2954,6 +2956,7 @@ const CONST = { MAPBOX: { PADDING: 50, DEFAULT_ZOOM: 10, + SINGLE_MARKER_ZOOM: 15, DEFAULT_COORDINATE: [-122.4021, 37.7911], STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq', }, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b6e62814466b..e8a860582bb1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -314,13 +314,13 @@ const ROUTES = { getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}` as const, }, MONEY_REQUEST_STEP_CONFIRMATION: { - route: 'create/:iouType/confirmation/:transactionID/:reportID/', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/confirmation/${transactionID}/${reportID}/` as const, + route: 'create/:iouType/confirmation/:transactionID/:reportID', + getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/confirmation/${transactionID}/${reportID}` as const, }, MONEY_REQUEST_STEP_AMOUNT: { - route: 'create/:iouType/amount/:transactionID/:reportID/', + route: 'create/:iouType/amount/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/amount/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/amount/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_TAX_RATE: { route: 'create/:iouType/taxRate/:transactionID/:reportID?', @@ -333,52 +333,52 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/taxAmount/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_CATEGORY: { - route: 'create/:iouType/category/:transactionID/:reportID/', + route: 'create/:iouType/category/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/category/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/category/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_CURRENCY: { - route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?/', + route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => getUrlWithBackToParam(`create/${iouType}/currency/${transactionID}/${reportID}/${pageIndex}`, backTo), }, MONEY_REQUEST_STEP_DATE: { - route: 'create/:iouType/date/:transactionID/:reportID/', + route: 'create/:iouType/date/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/date/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DESCRIPTION: { - route: 'create/:iouType/description/:transactionID/:reportID/', + route: 'create/:iouType/description/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DISTANCE: { - route: 'create/:iouType/distance/:transactionID/:reportID/', + route: 'create/:iouType/distance/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_MERCHANT: { - route: 'create/:iouType/merchant/:transactionID/:reportID/', + route: 'create/:iouType/merchant/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/merchant/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/merchant/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_PARTICIPANTS: { - route: 'create/:iouType/participants/:transactionID/:reportID/', + route: 'create/:iouType/participants/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/participants/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/participants/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_SCAN: { - route: 'create/:iouType/scan/:transactionID/:reportID/', + route: 'create/:iouType/scan/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/scan/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/scan/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_TAG: { - route: 'create/:iouType/tag/:transactionID/:reportID/', + route: 'create/:iouType/tag/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}/`, backTo), + getUrlWithBackToParam(`create/${iouType}/tag/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_WAYPOINT: { - route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex/', + route: 'create/:iouType/waypoint/:transactionID/:reportID/:pageIndex', getRoute: (iouType: ValueOf, transactionID: string, reportID: string, pageIndex = '', backTo = '') => getUrlWithBackToParam(`create/${iouType}/waypoint/${transactionID}/${reportID}/${pageIndex}`, backTo), }, diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index e5b605dd3ff2..1b4d350f7d4f 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -129,7 +129,7 @@ function AttachmentModal(props) { const [isAuthTokenRequired, setIsAuthTokenRequired] = useState(props.isAuthTokenRequired); const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); - const [source, setSource] = useState(props.source); + const [source, setSource] = useState(() => props.source); const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); const [confirmButtonFadeAnimation] = useState(() => new Animated.Value(1)); @@ -362,7 +362,7 @@ function AttachmentModal(props) { }, []); useEffect(() => { - setSource(props.source); + setSource(() => props.source); }, [props.source]); useEffect(() => { diff --git a/src/components/BaseMiniContextMenuItem.tsx b/src/components/BaseMiniContextMenuItem.tsx index 1f9a14cdfdee..7bed44cd8f13 100644 --- a/src/components/BaseMiniContextMenuItem.tsx +++ b/src/components/BaseMiniContextMenuItem.tsx @@ -8,6 +8,7 @@ import DomUtils from '@libs/DomUtils'; import getButtonState from '@libs/getButtonState'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; @@ -66,6 +67,7 @@ function BaseMiniContextMenuItem({tooltipText, onPress, children, isDelayButtonS event.preventDefault(); }} accessibilityLabel={tooltipText} + role={CONST.ROLE.BUTTON} style={({hovered, pressed}) => [ styles.reportActionContextMenuMiniButton, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, isDelayButtonStateComplete)), diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index dd0499d4d243..fb72f0cc845f 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -171,16 +171,16 @@ function Button( const keyboardShortcutCallback = useCallback( (event?: GestureResponderEvent | KeyboardEvent) => { - if (!validateSubmitShortcut(isFocused, isDisabled, isLoading, event)) { + if (!validateSubmitShortcut(isDisabled, isLoading, event)) { return; } onPress(); }, - [isDisabled, isFocused, isLoading, onPress], + [isDisabled, isLoading, onPress], ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, keyboardShortcutCallback, { - isActive: pressOnEnter && !shouldDisableEnterShortcut, + isActive: pressOnEnter && !shouldDisableEnterShortcut && isFocused, shouldBubble: allowBubble, priority: enterKeyEventListenerPriority, shouldPreventDefault: false, diff --git a/src/components/Button/validateSubmitShortcut/index.native.ts b/src/components/Button/validateSubmitShortcut/index.native.ts index 3f277ed208a1..4602b40c832f 100644 --- a/src/components/Button/validateSubmitShortcut/index.native.ts +++ b/src/components/Button/validateSubmitShortcut/index.native.ts @@ -3,14 +3,13 @@ import type ValidateSubmitShortcut from './types'; /** * Validate if the submit shortcut should be triggered depending on the button state * - * @param isFocused Whether Button is on active screen * @param isDisabled Indicates whether the button should be disabled * @param isLoading Indicates whether the button should be disabled and in the loading state * @return Returns `true` if the shortcut should be triggered */ -const validateSubmitShortcut: ValidateSubmitShortcut = (isFocused, isDisabled, isLoading) => { - if (!isFocused || isDisabled || isLoading) { +const validateSubmitShortcut: ValidateSubmitShortcut = (isDisabled, isLoading) => { + if (isDisabled || isLoading) { return false; } diff --git a/src/components/Button/validateSubmitShortcut/index.ts b/src/components/Button/validateSubmitShortcut/index.ts index 695c56af7bb7..f8cea44f73d6 100644 --- a/src/components/Button/validateSubmitShortcut/index.ts +++ b/src/components/Button/validateSubmitShortcut/index.ts @@ -3,16 +3,15 @@ import type ValidateSubmitShortcut from './types'; /** * Validate if the submit shortcut should be triggered depending on the button state * - * @param isFocused Whether Button is on active screen * @param isDisabled Indicates whether the button should be disabled * @param isLoading Indicates whether the button should be disabled and in the loading state * @param event Focused input event * @returns Returns `true` if the shortcut should be triggered */ -const validateSubmitShortcut: ValidateSubmitShortcut = (isFocused, isDisabled, isLoading, event) => { +const validateSubmitShortcut: ValidateSubmitShortcut = (isDisabled, isLoading, event) => { const eventTarget = event?.target as HTMLElement; - if (!isFocused || isDisabled || isLoading || eventTarget.nodeName === 'TEXTAREA') { + if (isDisabled || isLoading || eventTarget.nodeName === 'TEXTAREA') { return false; } diff --git a/src/components/Button/validateSubmitShortcut/types.ts b/src/components/Button/validateSubmitShortcut/types.ts index 088718d0334e..d1ff24fb0510 100644 --- a/src/components/Button/validateSubmitShortcut/types.ts +++ b/src/components/Button/validateSubmitShortcut/types.ts @@ -1,5 +1,5 @@ import type {GestureResponderEvent} from 'react-native'; -type ValidateSubmitShortcut = (isFocused: boolean, isDisabled: boolean, isLoading: boolean, event?: GestureResponderEvent | KeyboardEvent) => boolean; +type ValidateSubmitShortcut = (isDisabled: boolean, isLoading: boolean, event?: GestureResponderEvent | KeyboardEvent) => boolean; export default ValidateSubmitShortcut; diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index a25ccf184f52..602fb154deba 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -42,7 +42,7 @@ type CheckboxWithLabelProps = RequiredLabelProps & { /** Error text to display */ errorText?: string; - /** Value for checkbox. This prop is intended to be set by Form.js only */ + /** Value for checkbox. This prop is intended to be set by FormProvider only */ value?: boolean; /** The default value for the checkbox */ diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js index 571ddc820d43..f10af5e4a5a7 100644 --- a/src/components/DatePicker/CalendarPicker/index.js +++ b/src/components/DatePicker/CalendarPicker/index.js @@ -112,14 +112,44 @@ class CalendarPicker extends React.PureComponent { * Handles the user pressing the previous month arrow of the calendar picker. */ moveToPrevMonth() { - this.setState((prev) => ({currentDateView: subMonths(new Date(prev.currentDateView), 1)})); + this.setState((prev) => { + const prevMonth = subMonths(new Date(prev.currentDateView), 1); + // if year is subtracted, we need to update the years list + let newYears = prev.years; + if (prevMonth.getFullYear() < prev.currentDateView.getFullYear()) { + newYears = _.map(prev.years, (item) => ({ + ...item, + isSelected: item.value === prevMonth.getFullYear(), + })); + } + + return { + currentDateView: prevMonth, + years: newYears, + }; + }); } /** * Handles the user pressing the next month arrow of the calendar picker. */ moveToNextMonth() { - this.setState((prev) => ({currentDateView: addMonths(new Date(prev.currentDateView), 1)})); + this.setState((prev) => { + const nextMonth = addMonths(new Date(prev.currentDateView), 1); + // if year is added, we need to update the years list + let newYears = prev.years; + if (nextMonth.getFullYear() > prev.currentDateView.getFullYear()) { + newYears = _.map(prev.years, (item) => ({ + ...item, + isSelected: item.value === nextMonth.getFullYear(), + })); + } + + return { + currentDateView: nextMonth, + years: newYears, + }; + }); } render() { diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx index 6321b461f21e..a3178f642852 100644 --- a/src/components/MapView/MapView.tsx +++ b/src/components/MapView/MapView.tsx @@ -105,7 +105,7 @@ const MapView = forwardRef( if (waypoints.length === 1) { cameraRef.current?.setCamera({ - zoomLevel: 15, + zoomLevel: CONST.MAPBOX.SINGLE_MARKER_ZOOM, animationDuration: 1500, centerCoordinate: waypoints[0].coordinate, }); diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx index 05d86e8ec999..289f7d0d62a8 100644 --- a/src/components/MapView/MapView.website.tsx +++ b/src/components/MapView/MapView.website.tsx @@ -117,7 +117,7 @@ const MapView = forwardRef( if (waypoints.length === 1) { mapRef.flyTo({ center: waypoints[0].coordinate, - zoom: CONST.MAPBOX.DEFAULT_ZOOM, + zoom: CONST.MAPBOX.SINGLE_MARKER_ZOOM, }); return; } diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 34d60418d3ab..86e77ae4bfc3 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -35,20 +35,6 @@ import RenderHTML from './RenderHTML'; import SelectCircle from './SelectCircle'; import Text from './Text'; -type ResponsiveProps = { - /** Function to fire when component is pressed */ - onPress: (event: GestureResponderEvent | KeyboardEvent) => void; - - interactive?: true; -}; - -type UnresponsiveProps = { - onPress?: undefined; - - /** Whether the menu item should be interactive at all */ - interactive: false; -}; - type IconProps = { /** Flag to choose between avatar image or an icon */ iconType?: typeof CONST.ICON_TYPE_ICON; @@ -69,170 +55,175 @@ type NoIcon = { icon?: undefined; }; -type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & - (IconProps | AvatarProps | NoIcon) & { - /** Text to be shown as badge near the right end. */ - badgeText?: string; +type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { + /** Function to fire when component is pressed */ + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; - /** Used to apply offline styles to child text components */ - style?: ViewStyle; + /** Whether the menu item should be interactive at all */ + interactive?: boolean; - /** Any additional styles to apply */ - wrapperStyle?: StyleProp; + /** Text to be shown as badge near the right end. */ + badgeText?: string; - /** Any additional styles to apply on the outer element */ - containerStyle?: StyleProp; + /** Used to apply offline styles to child text components */ + style?: ViewStyle; - /** Used to apply styles specifically to the title */ - titleStyle?: ViewStyle; + /** Any additional styles to apply */ + wrapperStyle?: StyleProp; - /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle?: StyleProp>; + /** Any additional styles to apply on the outer element */ + containerStyle?: StyleProp; - /** Additional styles to style the description text below the title */ - descriptionTextStyle?: StyleProp; + /** Used to apply styles specifically to the title */ + titleStyle?: ViewStyle; - /** The fill color to pass into the icon. */ - iconFill?: string; + /** Any adjustments to style when menu item is hovered or pressed */ + hoverAndPressStyle?: StyleProp>; - /** Secondary icon to display on the left side of component, right of the icon */ - secondaryIcon?: IconAsset; + /** Additional styles to style the description text below the title */ + descriptionTextStyle?: StyleProp; - /** The fill color to pass into the secondary icon. */ - secondaryIconFill?: string; + /** The fill color to pass into the icon. */ + iconFill?: string; - /** Icon Width */ - iconWidth?: number; + /** Secondary icon to display on the left side of component, right of the icon */ + secondaryIcon?: IconAsset; - /** Icon Height */ - iconHeight?: number; + /** The fill color to pass into the secondary icon. */ + secondaryIconFill?: string; - /** Any additional styles to pass to the icon container. */ - iconStyles?: StyleProp; + /** Icon Width */ + iconWidth?: number; - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: IconAsset; + /** Icon Height */ + iconHeight?: number; - /** An icon to display under the main item */ - furtherDetailsIcon?: IconAsset; + /** Any additional styles to pass to the icon container. */ + iconStyles?: StyleProp; - /** Boolean whether to display the title right icon */ - shouldShowTitleIcon?: boolean; + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ + fallbackIcon?: IconAsset; - /** Icon to display at right side of title */ - titleIcon?: IconAsset; + /** An icon to display under the main item */ + furtherDetailsIcon?: IconAsset; - /** Boolean whether to display the right icon */ - shouldShowRightIcon?: boolean; + /** Boolean whether to display the title right icon */ + shouldShowTitleIcon?: boolean; - /** Overrides the icon for shouldShowRightIcon */ - iconRight?: IconAsset; + /** Icon to display at right side of title */ + titleIcon?: IconAsset; - /** Should render component on the right */ - shouldShowRightComponent?: boolean; + /** Boolean whether to display the right icon */ + shouldShowRightIcon?: boolean; - /** Component to be displayed on the right */ - rightComponent?: ReactNode; + /** Overrides the icon for shouldShowRightIcon */ + iconRight?: IconAsset; - /** A description text to show under the title */ - description?: string; + /** Should render component on the right */ + shouldShowRightComponent?: boolean; - /** Should the description be shown above the title (instead of the other way around) */ - shouldShowDescriptionOnTop?: boolean; + /** Component to be displayed on the right */ + rightComponent?: ReactNode; - /** Error to display below the title */ - error?: string; + /** A description text to show under the title */ + description?: string; - /** Error to display at the bottom of the component */ - errorText?: string; + /** Should the description be shown above the title (instead of the other way around) */ + shouldShowDescriptionOnTop?: boolean; - /** A boolean flag that gives the icon a green fill if true */ - success?: boolean; + /** Error to display below the title */ + error?: string; - /** Whether item is focused or active */ - focused?: boolean; + /** Error to display at the bottom of the component */ + errorText?: string; - /** Should we disable this menu item? */ - disabled?: boolean; + /** A boolean flag that gives the icon a green fill if true */ + success?: boolean; - /** Text that appears above the title */ - label?: string; + /** Whether item is focused or active */ + focused?: boolean; - /** Label to be displayed on the right */ - rightLabel?: string; + /** Should we disable this menu item? */ + disabled?: boolean; - /** Text to display for the item */ - title?: string; + /** Text that appears above the title */ + label?: string; - /** A right-aligned subtitle for this menu option */ - subtitle?: string | number; + /** Label to be displayed on the right */ + rightLabel?: string; - /** Should the title show with normal font weight (not bold) */ - shouldShowBasicTitle?: boolean; + /** Text to display for the item */ + title?: string; - /** Should we make this selectable with a checkbox */ - shouldShowSelectedState?: boolean; + /** A right-aligned subtitle for this menu option */ + subtitle?: string | number; - /** Whether this item is selected */ - isSelected?: boolean; + /** Should the title show with normal font weight (not bold) */ + shouldShowBasicTitle?: boolean; - /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally?: boolean; + /** Should we make this selectable with a checkbox */ + shouldShowSelectedState?: boolean; - /** Prop to represent the size of the avatar images to be shown */ - avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; + /** Whether this item is selected */ + isSelected?: boolean; - /** Avatars to show on the right of the menu item */ - floatRightAvatars?: IconType[]; + /** Prop to identify if we should load avatars vertically instead of diagonally */ + shouldStackHorizontally?: boolean; - /** Prop to represent the size of the float right avatar images to be shown */ - floatRightAvatarSize?: ValueOf; + /** Prop to represent the size of the avatar images to be shown */ + avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; - /** Affects avatar size */ - viewMode?: ValueOf; + /** Avatars to show on the right of the menu item */ + floatRightAvatars?: IconType[]; - /** Used to truncate the text with an ellipsis after computing the text layout */ - numberOfLinesTitle?: number; + /** Prop to represent the size of the float right avatar images to be shown */ + floatRightAvatarSize?: ValueOf; - /** Whether we should use small avatar subscript sizing the for menu item */ - isSmallAvatarSubscriptMenu?: boolean; + /** Affects avatar size */ + viewMode?: ValueOf; - /** The type of brick road indicator to show. */ - brickRoadIndicator?: ValueOf; + /** Used to truncate the text with an ellipsis after computing the text layout */ + numberOfLinesTitle?: number; - /** Should render the content in HTML format */ - shouldRenderAsHTML?: boolean; + /** Whether we should use small avatar subscript sizing the for menu item */ + isSmallAvatarSubscriptMenu?: boolean; - /** Should we grey out the menu item when it is disabled? */ - shouldGreyOutWhenDisabled?: boolean; + /** The type of brick road indicator to show. */ + brickRoadIndicator?: ValueOf; - /** The action accept for anonymous user or not */ - isAnonymousAction?: boolean; + /** Should render the content in HTML format */ + shouldRenderAsHTML?: boolean; - /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ - shouldBlockSelection?: boolean; + /** Should we grey out the menu item when it is disabled? */ + shouldGreyOutWhenDisabled?: boolean; - /** Whether should render title as HTML or as Text */ - shouldParseTitle?: false; + /** The action accept for anonymous user or not */ + isAnonymousAction?: boolean; - /** Should check anonymous user in onPress function */ - shouldCheckActionAllowedOnPress?: boolean; + /** Flag to indicate whether or not text selection should be disabled from long-pressing the menu item. */ + shouldBlockSelection?: boolean; - /** Text to display under the main item */ - furtherDetails?: string; + /** Whether should render title as HTML or as Text */ + shouldParseTitle?: false; - /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction?: () => void; + /** Should check anonymous user in onPress function */ + shouldCheckActionAllowedOnPress?: boolean; - /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips?: DisplayNameWithTooltip[]; + /** Text to display under the main item */ + furtherDetails?: string; - /** Icon should be displayed in its own color */ - displayInDefaultIconColor?: boolean; + /** The function that should be called when this component is LongPressed or right-clicked. */ + onSecondaryInteraction?: (event: GestureResponderEvent | MouseEvent) => void; - /** Determines how the icon should be resized to fit its container */ - contentFit?: ImageContentFit; - }; + /** Array of objects that map display names to their corresponding tooltip */ + titleWithTooltips?: DisplayNameWithTooltip[]; + + /** Icon should be displayed in its own color */ + displayInDefaultIconColor?: boolean; + + /** Determines how the icon should be resized to fit its container */ + contentFit?: ImageContentFit; +}; function MenuItem( { diff --git a/src/components/MenuItemList.js b/src/components/MenuItemList.js deleted file mode 100644 index c9eee8e888e1..000000000000 --- a/src/components/MenuItemList.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import _ from 'underscore'; -import useSingleExecution from '@hooks/useSingleExecution'; -import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import MenuItem from './MenuItem'; -import menuItemPropTypes from './menuItemPropTypes'; - -const propTypes = { - /** An array of props that are pass to individual MenuItem components */ - menuItems: PropTypes.arrayOf(PropTypes.shape(menuItemPropTypes)), - - /** Whether or not to use the single execution hook */ - shouldUseSingleExecution: PropTypes.bool, -}; -const defaultProps = { - menuItems: [], - shouldUseSingleExecution: false, -}; - -function MenuItemList(props) { - let popoverAnchor; - const {isExecuting, singleExecution} = useSingleExecution(); - - /** - * Handle the secondary interaction for a menu item. - * - * @param {*} link the menu item link or function to get the link - * @param {Event} e the interaction event - */ - const secondaryInteraction = (link, e) => { - if (typeof link === 'function') { - link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, url, popoverAnchor)); - } else if (!_.isEmpty(link)) { - ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, e, link, popoverAnchor); - } - }; - - return ( - <> - {_.map(props.menuItems, (menuItemProps) => ( - secondaryInteraction(menuItemProps.link, e) : undefined} - ref={(el) => (popoverAnchor = el)} - shouldBlockSelection={Boolean(menuItemProps.link)} - // eslint-disable-next-line react/jsx-props-no-spreading - {...menuItemProps} - disabled={menuItemProps.disabled || isExecuting} - onPress={props.shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} - /> - ))} - - ); -} - -MenuItemList.displayName = 'MenuItemList'; -MenuItemList.propTypes = propTypes; -MenuItemList.defaultProps = defaultProps; - -export default MenuItemList; diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx new file mode 100644 index 000000000000..f83f173a644f --- /dev/null +++ b/src/components/MenuItemList.tsx @@ -0,0 +1,63 @@ +import React, {useRef} from 'react'; +import type {GestureResponderEvent, View} from 'react-native'; +import useSingleExecution from '@hooks/useSingleExecution'; +import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {MenuItemProps} from './MenuItem'; +import MenuItem from './MenuItem'; + +type MenuItemLink = string | (() => Promise); + +type MenuItemWithLink = MenuItemProps & { + /** The link to open when the menu item is clicked */ + link: MenuItemLink; +}; + +type MenuItemListProps = { + /** An array of props that are pass to individual MenuItem components */ + menuItems: MenuItemWithLink[]; + + /** Whether or not to use the single execution hook */ + shouldUseSingleExecution?: boolean; +}; + +function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuItemListProps) { + const popoverAnchor = useRef(null); + const {isExecuting, singleExecution} = useSingleExecution(); + + /** + * Handle the secondary interaction for a menu item. + * + * @param link the menu item link or function to get the link + * @param event the interaction event + */ + const secondaryInteraction = (link: MenuItemLink, event: GestureResponderEvent | MouseEvent) => { + if (typeof link === 'function') { + link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, url, popoverAnchor.current)); + } else if (link) { + ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, link, popoverAnchor.current); + } + }; + + return ( + <> + {menuItems.map((menuItemProps) => ( + secondaryInteraction(menuItemProps.link, e) : undefined} + ref={popoverAnchor} + shouldBlockSelection={!!menuItemProps.link} + // eslint-disable-next-line react/jsx-props-no-spreading + {...menuItemProps} + disabled={!!menuItemProps.disabled || isExecuting} + onPress={shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress} + /> + ))} + + ); +} + +MenuItemList.displayName = 'MenuItemList'; + +export type {MenuItemWithLink}; +export default MenuItemList; diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 7ec95aec951f..2fee67a3d632 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -277,7 +277,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowTags = isPolicyExpenseChat && OptionsListUtils.hasEnabledOptions(_.values(policyTagList)); // A flag for showing tax rate - const shouldShowTax = isPolicyExpenseChat && policy.isTaxTrackingEnabled; + const shouldShowTax = isPolicyExpenseChat && policy && policy.isTaxTrackingEnabled; // A flag for showing the billable field const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true); diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index a7728045f407..414f030d4fc7 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -123,7 +123,10 @@ function TaskPreview(props) { style={[styles.mr2]} containerStyle={[styles.taskCheckbox]} isChecked={isTaskCompleted} - disabled={!Task.canModifyTask(props.taskReport, props.currentUserPersonalDetails.accountID, lodashGet(props.rootParentReportpolicy, 'role', ''))} + disabled={ + _.isEmpty(props.taskReport) || + !Task.canModifyTask(props.taskReport, props.currentUserPersonalDetails.accountID, lodashGet(props.rootParentReportpolicy, 'role', '')) + } onPress={Session.checkIfActionIsAllowed(() => { if (isTaskCompleted) { Task.reopenTask(props.taskReport); diff --git a/src/components/Section/IconSection.js b/src/components/Section/IconSection.js deleted file mode 100644 index 307331aa36d6..000000000000 --- a/src/components/Section/IconSection.js +++ /dev/null @@ -1,41 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import Icon from '@components/Icon'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; -import useThemeStyles from '@hooks/useThemeStyles'; - -const iconSectionPropTypes = { - icon: sourcePropTypes, - IconComponent: PropTypes.IconComponent, - iconContainerStyles: PropTypes.iconContainerStyles, -}; - -const defaultIconSectionPropTypes = { - icon: null, - IconComponent: null, - iconContainerStyles: [], -}; - -function IconSection({icon, IconComponent, iconContainerStyles}) { - const styles = useThemeStyles(); - - return ( - - {Boolean(icon) && ( - - )} - {Boolean(IconComponent) && } - - ); -} - -IconSection.displayName = 'IconSection'; -IconSection.propTypes = iconSectionPropTypes; -IconSection.defaultProps = defaultIconSectionPropTypes; - -export default IconSection; diff --git a/src/components/Section/IconSection.tsx b/src/components/Section/IconSection.tsx new file mode 100644 index 000000000000..cc42c6b7ace5 --- /dev/null +++ b/src/components/Section/IconSection.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type IconSectionProps = { + icon?: IconAsset; + iconContainerStyles?: StyleProp; +}; + +function IconSection({icon, iconContainerStyles}: IconSectionProps) { + const styles = useThemeStyles(); + + return ( + + {!!icon && ( + + )} + + ); +} + +IconSection.displayName = 'IconSection'; + +export default IconSection; diff --git a/src/components/Section/index.js b/src/components/Section/index.js deleted file mode 100644 index 50576abef025..000000000000 --- a/src/components/Section/index.js +++ /dev/null @@ -1,122 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; -import MenuItemList from '@components/MenuItemList'; -import menuItemPropTypes from '@components/menuItemPropTypes'; -import Text from '@components/Text'; -import useThemeStyles from '@hooks/useThemeStyles'; -import IconSection from './IconSection'; - -const CARD_LAYOUT = { - ICON_ON_TOP: 'iconOnTop', - ICON_ON_RIGHT: 'iconOnRight', -}; - -const propTypes = { - /** An array of props that are pass to individual MenuItem components */ - menuItems: PropTypes.arrayOf(PropTypes.shape(menuItemPropTypes)), - - /** The text to display in the title of the section */ - title: PropTypes.string.isRequired, - - /** The text to display in the subtitle of the section */ - subtitle: PropTypes.string, - - /** The icon to display along with the title */ - icon: sourcePropTypes, - - /** Icon component */ - IconComponent: PropTypes.func, - - /** Card layout that affects icon positioning, margins, sizes. */ - // eslint-disable-next-line rulesdir/prefer-underscore-method - cardLayout: PropTypes.oneOf(Object.values(CARD_LAYOUT)), - - /** Contents to display inside the section */ - children: PropTypes.node, - - /** Customize the Section container */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the Section container */ - // eslint-disable-next-line react/forbid-prop-types - titleStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the Section container */ - // eslint-disable-next-line react/forbid-prop-types - subtitleStyles: PropTypes.arrayOf(PropTypes.object), - - /** Whether the subtitle should have a muted style */ - subtitleMuted: PropTypes.bool, - - /** Customize the Section container */ - // eslint-disable-next-line react/forbid-prop-types - childrenStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the Icon container */ - // eslint-disable-next-line react/forbid-prop-types - iconContainerStyles: PropTypes.arrayOf(PropTypes.object), -}; - -const defaultProps = { - menuItems: null, - children: null, - icon: null, - IconComponent: null, - cardLayout: CARD_LAYOUT.ICON_ON_RIGHT, - containerStyles: [], - iconContainerStyles: [], - titleStyles: [], - subtitleStyles: [], - subtitleMuted: false, - childrenStyles: [], - subtitle: null, -}; - -function Section({children, childrenStyles, containerStyles, icon, IconComponent, cardLayout, iconContainerStyles, menuItems, subtitle, subtitleStyles, subtitleMuted, title, titleStyles}) { - const styles = useThemeStyles(); - - return ( - <> - - {cardLayout === CARD_LAYOUT.ICON_ON_TOP && ( - - )} - - - {title} - - {cardLayout === CARD_LAYOUT.ICON_ON_RIGHT && ( - - )} - - - {Boolean(subtitle) && ( - - {subtitle} - - )} - - {children} - - {Boolean(menuItems) && } - - - ); -} -Section.displayName = 'Section'; -Section.propTypes = propTypes; -Section.defaultProps = defaultProps; - -export {CARD_LAYOUT}; -export default Section; diff --git a/src/components/Section/index.tsx b/src/components/Section/index.tsx new file mode 100644 index 000000000000..f24316a5f1bb --- /dev/null +++ b/src/components/Section/index.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import type {MenuItemWithLink} from '@components/MenuItemList'; +import MenuItemList from '@components/MenuItemList'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; +import type IconAsset from '@src/types/utils/IconAsset'; +import IconSection from './IconSection'; + +const CARD_LAYOUT = { + ICON_ON_TOP: 'iconOnTop', + ICON_ON_RIGHT: 'iconOnRight', +} as const; + +type SectionProps = ChildrenProps & { + /** An array of props that are passed to individual MenuItem components */ + menuItems?: MenuItemWithLink[]; + + /** The text to display in the title of the section */ + title: string; + + /** The text to display in the subtitle of the section */ + subtitle?: string; + + /** The icon to display along with the title */ + icon?: IconAsset; + + /** Card layout that affects icon positioning, margins, sizes */ + cardLayout?: ValueOf; + + /** Whether the subtitle should have a muted style */ + subtitleMuted?: boolean; + + /** Customize the Section container */ + containerStyles?: StyleProp; + + /** Customize the Section container */ + titleStyles?: StyleProp; + + /** Customize the Section container */ + subtitleStyles?: StyleProp; + + /** Customize the Section container */ + childrenStyles?: StyleProp; + + /** Customize the Icon container */ + iconContainerStyles?: StyleProp; +}; + +function Section({ + children, + childrenStyles, + containerStyles, + icon, + cardLayout = CARD_LAYOUT.ICON_ON_RIGHT, + iconContainerStyles, + menuItems, + subtitle, + subtitleStyles, + subtitleMuted = false, + title, + titleStyles, +}: SectionProps) { + const styles = useThemeStyles(); + + return ( + <> + + {cardLayout === CARD_LAYOUT.ICON_ON_TOP && ( + + )} + + + {title} + + {cardLayout === CARD_LAYOUT.ICON_ON_RIGHT && ( + + )} + + + {!!subtitle && ( + + {subtitle} + + )} + + {children} + + {!!menuItems && } + + + ); +} +Section.displayName = 'Section'; + +export {CARD_LAYOUT}; +export default Section; diff --git a/src/hooks/useSingleExecution/index.ts b/src/hooks/useSingleExecution/index.ts index f1be359f0355..909416dd848b 100644 --- a/src/hooks/useSingleExecution/index.ts +++ b/src/hooks/useSingleExecution/index.ts @@ -9,9 +9,9 @@ type Action = (...params: T) => void | Promise; */ export default function useSingleExecution() { const singleExecution = useCallback( - (action: Action) => + (action?: Action) => (...params: T) => { - action(...params); + action?.(...params); }, [], ); diff --git a/src/languages/en.ts b/src/languages/en.ts index 0b8983a8361b..83d244262e8a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -635,6 +635,7 @@ export default { genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later', genericEditFailureMessage: 'Unexpected error editing the money request, please try again later', genericSmartscanFailureMessage: 'Transaction is missing fields', + duplicateWaypointsErrorMessage: 'Please remove duplicate waypoints', atLeastTwoDifferentWaypoints: 'Please enter at least two different addresses', splitBillMultipleParticipantsErrorMessage: 'Split bill is only allowed between a single workspace or individual users. Please update your selection.', invalidMerchant: 'Please enter a correct merchant.', diff --git a/src/languages/es.ts b/src/languages/es.ts index a1afde53482b..93b19246d8ca 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -630,6 +630,7 @@ export default { genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde', genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde', genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', + duplicateWaypointsErrorMessage: 'Por favor elimina los puntos de ruta duplicados', atLeastTwoDifferentWaypoints: 'Por favor introduce al menos dos direcciones diferentes', splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor actualiza tu selección.', invalidMerchant: 'Por favor ingrese un comerciante correcto.', diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index cc77cd1c4908..99321d7734c6 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -125,7 +125,8 @@ function isActiveRoute(routePath: Route): boolean { let activeRoute = getActiveRoute(); activeRoute = activeRoute.startsWith('/') ? activeRoute.substring(1) : activeRoute; - return activeRoute === routePath; + // We remove redundant (consecutive and trailing) slashes from path before matching + return activeRoute === routePath.replace(CONST.REGEX.ROUTES.REDUNDANT_SLASHES, (match, p1) => (p1 ? '/' : '')); } /** diff --git a/src/libs/Network/enhanceParameters.ts b/src/libs/Network/enhanceParameters.ts index 3fadeea7447c..e14acda5ff56 100644 --- a/src/libs/Network/enhanceParameters.ts +++ b/src/libs/Network/enhanceParameters.ts @@ -1,3 +1,4 @@ +import * as Environment from '@libs/Environment/Environment'; import getPlatform from '@libs/getPlatform'; import CONFIG from '@src/CONFIG'; import * as NetworkStore from './NetworkStore'; @@ -37,6 +38,8 @@ export default function enhanceParameters(command: string, parameters: Record 0; + return hasReplies && !!reportAction.childCommenterCount && !isThreadFirstChat(reportAction, reportID); +} + /** * Disable reply in thread action if: * @@ -4516,6 +4524,7 @@ export { canEditWriteCapability, hasSmartscanError, shouldAutoFocusOnKeyPress, + shouldDisplayThreadReplies, shouldDisableThread, }; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 4b973d95d136..688e22f453cd 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -317,7 +317,8 @@ function isValidDisplayName(name: string): boolean { * Checks that the provided legal name doesn't contain special characters */ function isValidLegalName(name: string): boolean { - return CONST.REGEX.ALPHABETIC_AND_LATIN_CHARS.test(name); + const hasAccentedChars = Boolean(name.match(CONST.REGEX.ACCENT_LATIN_CHARS)); + return CONST.REGEX.ALPHABETIC_AND_LATIN_CHARS.test(name) && !hasAccentedChars; } /** diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index fb4e9f02f1b6..f35cc522ba76 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2628,7 +2628,10 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, value: { - [reportPreviewAction.reportActionID]: reportPreviewAction, + [reportPreviewAction.reportActionID]: { + ...reportPreviewAction, + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericDeleteFailureMessage'), + }, }, }, ...(shouldDeleteIOUReport diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 2291e6d0af4a..a0772db49585 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -7,6 +7,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OnyxUpdateEvent, OnyxUpdatesFromServer, Request} from '@src/types/onyx'; import type Response from '@src/types/onyx/Response'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as QueuedOnyxUpdates from './QueuedOnyxUpdates'; // This key needs to be separate from ONYXKEYS.ONYX_UPDATES_FROM_SERVER so that it can be updated without triggering the callback when the server IDs are updated. If that @@ -74,7 +75,18 @@ function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFrom Log.info(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, false, {command: request?.command}); if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) <= lastUpdateIDAppliedToClient) { - Log.info('[OnyxUpdateManager] Update received was older or the same than current state, returning without applying the updates', false); + Log.info('[OnyxUpdateManager] Update received was older than or the same as current state, returning without applying the updates other than successData and failureData'); + + // In this case, we're already received the OnyxUpdate included in the response, so we don't need to apply it again. + // However, we do need to apply the successData and failureData from the request + if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response && (!isEmptyObject(request.successData) || !isEmptyObject(request.failureData))) { + Log.info('[OnyxUpdateManager] Applying success or failure data from request without onyxData from response'); + + // We use a spread here instead of delete because we don't want to change the response for other middlewares + const {onyxData, ...responseWithoutOnyxData} = response; + return applyHTTPSOnyxUpdates(request, responseWithoutOnyxData); + } + return Promise.resolve(); } if (lastUpdateID && (lastUpdateIDAppliedToClient === null || Number(lastUpdateID) > lastUpdateIDAppliedToClient)) { diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 212fd3ead898..27f879626f8b 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1,4 +1,5 @@ import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST'; +import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import {escapeRegExp} from 'lodash'; import filter from 'lodash/filter'; @@ -616,10 +617,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs, welcomeNote, policyID) const params = { employees: JSON.stringify(_.map(logins, (login) => ({email: login}))), - - // Do not escape HTML special chars for welcomeNote as this will be handled in the backend. - // See https://github.com/Expensify/App/issues/20081 for more details. - welcomeNote, + welcomeNote: new ExpensiMark().replace(welcomeNote), policyID, }; if (!_.isEmpty(membersChats.reportCreationData)) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 043f99265b5f..cef236a3e1bb 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2120,6 +2120,24 @@ function leaveRoom(reportID: string, isWorkspaceMemberLeavingWorkspaceRoom = fal }, ]; + if (report.parentReportID && report.parentReportActionID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[report.parentReportActionID]: {childReportNotificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}}, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[report.parentReportActionID]: {childReportNotificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[report.parentReportActionID]: {childReportNotificationPreference: report.notificationPreference}}, + }); + } + type LeaveRoomParameters = { reportID: string; }; diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js index d6f21f3ecdca..18da2c11a0e6 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.js +++ b/src/pages/EnablePayments/AdditionalDetailsStep.js @@ -87,7 +87,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP const shouldAskForFullSSN = walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN; /** - * @param {Object} values The values object is passed from Form.js and contains info for each form element that has an inputID + * @param {Object} values The values object is passed from FormProvider and contains info for each form element that has an inputID * @returns {Object} */ const validate = (values) => { @@ -128,7 +128,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP }; /** - * @param {Object} values The values object is passed from Form.js and contains info for each form element that has an inputID + * @param {Object} values The values object is passed from FormProvider and contains info for each form element that has an inputID */ const activateWallet = (values) => { const personalDetails = { diff --git a/src/pages/ErrorPage/GenericErrorPage.js b/src/pages/ErrorPage/GenericErrorPage.js index 3b1b9ef9567c..56fb5b970084 100644 --- a/src/pages/ErrorPage/GenericErrorPage.js +++ b/src/pages/ErrorPage/GenericErrorPage.js @@ -85,7 +85,7 @@ function GenericErrorPage({translate}) { src={LogoWordmark} height={30} width={80} - fill={theme.textLight} + fill={theme.text} /> diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index b2e74a2b7cbf..f22eda58ce7f 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -161,12 +161,14 @@ export default [ const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; } + const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction); + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(reportAction, reportID); const subscribed = childReportNotificationPreference !== 'hidden'; const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); - return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction); + return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction) && (!isDeletedAction || shouldDisplayThreadReplies); }, onPress: (closePopover, {reportAction, reportID}) => { let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); @@ -199,6 +201,8 @@ export default [ const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; } + const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction); + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(reportAction, reportID); const subscribed = childReportNotificationPreference !== 'hidden'; if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; @@ -206,7 +210,7 @@ export default [ const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); - return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); + return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction) && (!isDeletedAction || shouldDisplayThreadReplies); }, onPress: (closePopover, {reportAction, reportID}) => { let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index 76af9d4fccb0..5b64d90da5da 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -18,7 +18,7 @@ type ShowContextMenu = ( type: ContextMenuType, event: GestureResponderEvent | MouseEvent, selection: string, - contextMenuAnchor: RNText | null, + contextMenuAnchor: View | RNText | null, reportID?: string, reportActionID?: string, originalReportID?: string, @@ -96,7 +96,7 @@ function showContextMenu( type: ContextMenuType, event: GestureResponderEvent | MouseEvent, selection: string, - contextMenuAnchor: RNText | null, + contextMenuAnchor: View | RNText | null, reportID = '0', reportActionID = '0', originalReportID = '0', diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 1e8439c0086b..6c1d71625dc9 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -5,7 +5,6 @@ import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-nat import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Composer from '@components/Composer'; -import {PopoverContext} from '@components/PopoverProvider'; import withKeyboardState from '@components/withKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; @@ -108,7 +107,6 @@ function ComposerWithSuggestions({ // For testing children, }) { - const {isOpen: isPopoverOpen} = React.useContext(PopoverContext); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -411,15 +409,9 @@ function ComposerWithSuggestions({ * @param {Boolean} [shouldDelay=false] Impose delay before focusing the composer * @memberof ReportActionCompose */ - const focus = useCallback( - (shouldDelay = false) => { - if (isPopoverOpen) { - return; - } - focusComposerWithDelay(textInputRef.current)(shouldDelay); - }, - [isPopoverOpen], - ); + const focus = useCallback((shouldDelay = false) => { + focusComposerWithDelay(textInputRef.current)(shouldDelay); + }, []); const setUpComposeFocusManager = useCallback(() => { // This callback is used in the contextMenuActions to manage giving focus back to the compose input. diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 4a060abb762a..a412fb2aa0bd 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -487,9 +487,8 @@ function ReportActionItem(props) { ); } const numberOfThreadReplies = _.get(props, ['action', 'childVisibleActionCount'], 0); - const hasReplies = numberOfThreadReplies > 0; - const shouldDisplayThreadReplies = hasReplies && props.action.childCommenterCount && !ReportUtils.isThreadFirstChat(props.action, props.report.reportID); + const shouldDisplayThreadReplies = ReportUtils.shouldDisplayThreadReplies(props.action, props.report.reportID); const oldestFourAccountIDs = _.map(lodashGet(props.action, 'childOldestFourAccountIDs', '').split(','), (accountID) => Number(accountID)); const draftMessageRightAlign = !_.isUndefined(props.draftMessage) ? styles.chatItemReactionsDraftRight : {}; diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 66cbd7f135a9..ddf692fedd46 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -131,6 +131,10 @@ function IOURequestStepDistance({ return ErrorUtils.getLatestErrorField(transaction, 'route'); } + if (_.keys(waypoints).length > 2 && _.size(validatedWaypoints) !== _.keys(waypoints).length) { + return {0: translate('iou.error.duplicateWaypointsErrorMessage')}; + } + if (_.size(validatedWaypoints) < 2) { return {0: translate('iou.error.atLeastTwoDifferentWaypoints')}; } @@ -158,12 +162,12 @@ function IOURequestStepDistance({ const submitWaypoints = useCallback(() => { // If there is any error or loading state, don't let user go to next page. - if (_.size(validatedWaypoints) < 2 || hasRouteError || isLoadingRoute || isLoading) { + if (_.size(validatedWaypoints) < 2 || (_.keys(waypoints).length > 2 && _.size(validatedWaypoints) !== _.keys(waypoints).length) || hasRouteError || isLoadingRoute || isLoading) { setHasError(true); return; } navigateToNextStep(); - }, [setHasError, hasRouteError, isLoadingRoute, isLoading, validatedWaypoints, navigateToNextStep]); + }, [setHasError, waypoints, hasRouteError, isLoadingRoute, isLoading, validatedWaypoints, navigateToNextStep]); return ( {/* Show error message if there is route error or there are less than 2 routes and user has tried submitting, */} - {((hasError && _.size(validatedWaypoints) < 2) || hasRouteError) && ( + {((hasError && _.size(validatedWaypoints) < 2) || (_.keys(waypoints).length > 2 && _.size(validatedWaypoints) !== _.keys(waypoints).length) || hasRouteError) && ( { - Navigation.goBack(isEditing ? ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID) : ROUTES.HOME); + Navigation.goBack(backTo || ROUTES.HOME); }; const navigateToCurrencySelectionPage = () => { diff --git a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js index bae08cd8cb62..cf91ac1e1812 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxRatePage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxRatePage.js @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -16,21 +15,13 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import * as IOU from '@userActions/IOU'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; const propTypes = { - /** Route from navigation */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - }), - }).isRequired, + /** Navigation route context info provided by react navigation */ + route: IOURequestStepRoutePropTypes.isRequired, /* Onyx Props */ /** Collection of tax rates attached to a policy */ @@ -52,16 +43,16 @@ const getTaxAmount = (taxRates, selectedTaxRate, amount) => { function IOURequestStepTaxRatePage({ route: { - params: {iouType, reportID}, + params: {backTo}, }, policyTaxRates, transaction, }) { const {translate} = useLocalize(); - function navigateBack() { - Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); - } + const navigateBack = () => { + Navigation.goBack(backTo || ROUTES.HOME); + }; const defaultTaxKey = policyTaxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; @@ -73,7 +64,7 @@ function IOURequestStepTaxRatePage({ IOU.setMoneyRequestTaxRate(transaction.transactionID, taxes); IOU.setMoneyRequestTaxAmount(transaction.transactionID, amountInSmallestCurrencyUnits); - Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, reportID)); + Navigation.goBack(backTo || ROUTES.HOME); }; return ( diff --git a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js index 1d1ce906189b..cd1f4591a61a 100644 --- a/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js +++ b/src/pages/settings/Wallet/Card/BaseGetPhysicalCard.js @@ -3,7 +3,7 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {Text} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import Form from '@components/Form'; +import FormProvider from '@components/Form/FormProvider'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -114,7 +114,7 @@ const defaultProps = { loginList: {}, isConfirmation: false, renderContent: (onSubmit, submitButtonText, styles, children = () => {}, onValidate = () => ({})) => ( -
{children} -
+ ), onValidate: () => ({}), }; diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardName.js b/src/pages/settings/Wallet/Card/GetPhysicalCardName.js index 0040dac8b75f..5b954d432cce 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardName.js +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardName.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import InputWrapper from '@components/Form/InputWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -72,7 +73,8 @@ function GetPhysicalCardName({ title={translate('getPhysicalCard.header')} onValidate={onValidate} > - - - { - const isLoading = props.reimbursementAccount.isLoading || false; - if (isLoading !== shouldShowLoadingSpinner) { - setShouldShowLoadingSpinner(isLoading); - } - }, CONST.TIMING.SHOW_LOADING_SPINNER_DEBOUNCE_TIME); + const isLoading = lodashGet(props.reimbursementAccount, 'isLoading', false); + const prevIsLoading = usePrevious(isLoading); + useEffect(() => { - debounceSetShouldShowLoadingSpinner(); - }, [debounceSetShouldShowLoadingSpinner]); + if (prevIsLoading === isLoading) { + return; + } + setShouldShowLoadingSpinner(isLoading); + }, [prevIsLoading, isLoading]); if (props.network.isOffline) { return ( @@ -62,11 +61,6 @@ function WorkspaceReimburseSection(props) { ); } - // If the reimbursementAccount is loading but not enough time has passed to show a spinner, then render nothing. - if (props.reimbursementAccount.isLoading && !shouldShowLoadingSpinner) { - return null; - } - if (shouldShowLoadingSpinner) { return ( diff --git a/src/styles/index.ts b/src/styles/index.ts index 6d3cbd93c6c8..a6ac2e269eb0 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -2094,7 +2094,7 @@ const styles = (theme: ThemeColors) => }, avatarInnerTextSmall: { - color: theme.textLight, + color: theme.text, fontSize: variables.fontSizeExtraSmall, lineHeight: undefined, marginLeft: -2, @@ -2450,7 +2450,7 @@ const styles = (theme: ThemeColors) => RHPNavigatorContainerNavigatorContainerStyles: (isSmallScreenWidth: boolean) => ({marginLeft: isSmallScreenWidth ? 0 : variables.sideBarWidth, flex: 1} satisfies ViewStyle), avatarInnerTextChat: { - color: theme.textLight, + color: theme.text, fontSize: variables.fontSizeXLarge, fontFamily: FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM, textAlign: 'center', diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index e2d237c6bbae..1a7f9b2b0a8d 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1351,6 +1351,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), ...positioning.r4, ...styles.cursorDefault, + ...styles.userSelectNone, position: 'absolute', zIndex: 8, }), diff --git a/tests/unit/enhanceParametersTest.js b/tests/unit/enhanceParametersTest.js index 513206b42614..6829732f1633 100644 --- a/tests/unit/enhanceParametersTest.js +++ b/tests/unit/enhanceParametersTest.js @@ -18,6 +18,7 @@ test('Enhance parameters adds correct parameters for Log command with no authTok testParameter: 'test', api_setCookie: false, email, + isFromDevEnv: true, platform: 'ios', referer: CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER, }); @@ -36,6 +37,7 @@ test('Enhance parameters adds correct parameters for a command that requires aut testParameter: 'test', api_setCookie: false, email, + isFromDevEnv: true, platform: 'ios', authToken, referer: CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER,