+
+You can see which devices have been used to access your Expensify account and even remove devices that you no longer want to have access to your account.
+
+{% include info.html %}
+This process is currently not available from the mobile app and must be completed from the Expensify website.
+{% include end-info.html %}
+
+1. Hover over Settings and click **Account**.
+2. Under Account Details, scroll down to the Device Management section.
+3. Click **Device Management** to expand the section.
+4. Review the devices that have access to your account. To remove access for a specific device, click **Revoke** next to it.
+
+
diff --git a/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md b/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md
new file mode 100644
index 000000000000..2d561ea598d9
--- /dev/null
+++ b/docs/articles/expensify-classic/settings/account-settings/Set-notifications.md
@@ -0,0 +1,15 @@
+---
+title: Set notifications
+description: Select your Expensify notification preferences
+---
+
+
+{% include info.html %}
+This process is currently not available from the mobile app and must be completed from the Expensify website.
+{% include end-info.html %}
+
+1. Hover over Settings and click **Account**.
+2. Click the **Preferences** tab on the left.
+3. Scroll down to the Contact Preferences section.
+4. Select the checkbox for the types of notifications you wish to receive.
+
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 76b7bac3fc99..8e160e3bcdf2 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -54,3 +54,4 @@ https://help.expensify.com/articles/expensify-classic/getting-started/Employees,
https://help.expensify.com/articles/expensify-classic/getting-started/Using-The-App,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace
https://help.expensify.com/articles/expensify-classic/getting-started/support/Expensify-Support,https://use.expensify.com/support
https://help.expensify.com/articles/expensify-classic/getting-started/Plan-Types,https://use.expensify.com/
+https://help.expensify.com/articles/new-expensify/payments/Referral-Program,https://help.expensify.com/articles/expensify-classic/get-paid-back/Referral-Program
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 1eca5a5021f7..93faff6ab427 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.43.5
+ 1.4.43.9ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index e4453165b29a..85d5f45e4184 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature????CFBundleVersion
- 1.4.43.5
+ 1.4.43.9
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 1bb165389913..6b0cc0c08d14 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString1.4.43CFBundleVersion
- 1.4.43.5
+ 1.4.43.9NSExtensionNSExtensionPointIdentifier
diff --git a/jest.config.js b/jest.config.js
index 95ecc350ed9f..441507af4228 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -8,7 +8,7 @@ module.exports = {
`/?(*.)+(spec|test).${testFileExtension}`,
],
transform: {
- '^.+\\.jsx?$': 'babel-jest',
+ '^.+\\.[jt]sx?$': 'babel-jest',
'^.+\\.svg?$': 'jest-transformer-svg',
},
transformIgnorePatterns: ['/node_modules/(?!react-native)/'],
diff --git a/package-lock.json b/package-lock.json
index 93951ba0f241..c114de61408f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.43-5",
+ "version": "1.4.43-9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.43-5",
+ "version": "1.4.43-9",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 76e3f689ca39..66d60bcd87cd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.43-5",
+ "version": "1.4.43-9",
"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.",
@@ -50,8 +50,8 @@
"analyze-packages": "ANALYZE_BUNDLE=true webpack --config config/webpack/webpack.common.js --env envFile=.env.production",
"symbolicate:android": "npx metro-symbolicate android/app/build/generated/sourcemaps/react/release/index.android.bundle.map",
"symbolicate:ios": "npx metro-symbolicate main.jsbundle.map",
- "test:e2e": "ts-node tests/e2e/testRunner.js --development --skipCheckout --skipInstallDeps --buildMode none",
- "test:e2e:dev": "ts-node tests/e2e/testRunner.js --development --skipCheckout --config ./config.dev.js --buildMode skip --skipInstallDeps",
+ "test:e2e": "ts-node tests/e2e/testRunner.js --config ./config.local.ts",
+ "test:e2e:dev": "ts-node tests/e2e/testRunner.js --config ./config.dev.js",
"gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh",
"workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh",
"workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js",
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index c5480d363019..c41ef521661c 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -84,28 +84,28 @@ const ROUTES = {
SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links',
SETTINGS_WALLET: 'settings/wallet',
SETTINGS_WALLET_DOMAINCARD: {
- route: '/settings/wallet/card/:domain',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}` as const,
+ route: 'settings/wallet/card/:domain',
+ getRoute: (domain: string) => `settings/wallet/card/${domain}` as const,
},
SETTINGS_REPORT_FRAUD: {
- route: '/settings/wallet/card/:domain/report-virtual-fraud',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-virtual-fraud` as const,
+ route: 'settings/wallet/card/:domain/report-virtual-fraud',
+ getRoute: (domain: string) => `settings/wallet/card/${domain}/report-virtual-fraud` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: {
- route: '/settings/wallet/card/:domain/get-physical/name',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/name` as const,
+ route: 'settings/wallet/card/:domain/get-physical/name',
+ getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/name` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE: {
- route: '/settings/wallet/card/:domain/get-physical/phone',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/phone` as const,
+ route: 'settings/wallet/card/:domain/get-physical/phone',
+ getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/phone` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS: {
- route: '/settings/wallet/card/:domain/get-physical/address',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/address` as const,
+ route: 'settings/wallet/card/:domain/get-physical/address',
+ getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/address` as const,
},
SETTINGS_WALLET_CARD_GET_PHYSICAL_CONFIRM: {
- route: '/settings/wallet/card/:domain/get-physical/confirm',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/confirm` as const,
+ route: 'settings/wallet/card/:domain/get-physical/confirm',
+ getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/confirm` as const,
},
SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card',
SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account',
@@ -117,8 +117,8 @@ const ROUTES = {
SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance',
SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account',
SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: {
- route: '/settings/wallet/card/:domain/report-card-lost-or-damaged',
- getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-card-lost-or-damaged` as const,
+ route: 'settings/wallet/card/:domain/report-card-lost-or-damaged',
+ getRoute: (domain: string) => `settings/wallet/card/${domain}/report-card-lost-or-damaged` as const,
},
SETTINGS_WALLET_CARD_ACTIVATE: {
route: 'settings/wallet/card/:domain/activate',
@@ -219,6 +219,10 @@ const ROUTES = {
route: 'r/:reportID/settings/who-can-post',
getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` as const,
},
+ REPORT_SETTINGS_VISIBILITY: {
+ route: 'r/:reportID/settings/visibility',
+ getRoute: (reportID: string) => `r/${reportID}/settings/visibility` as const,
+ },
SPLIT_BILL_DETAILS: {
route: 'r/:reportID/split/:reportActionID',
getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index ee3c64e8d804..18754a3513c1 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -167,6 +167,7 @@ const SCREENS = {
ROOM_NAME: 'Report_Settings_Room_Name',
NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences',
WRITE_CAPABILITY: 'Report_Settings_Write_Capability',
+ VISIBILITY: 'Report_Settings_Visibility',
},
NEW_TASK: {
diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx
index 25dc99459064..5b5e99ac0621 100644
--- a/src/components/CountrySelector.tsx
+++ b/src/components/CountrySelector.tsx
@@ -1,4 +1,5 @@
-import React, {forwardRef, useEffect} from 'react';
+import {useIsFocused} from '@react-navigation/native';
+import React, {forwardRef, useEffect, useRef} from 'react';
import type {ForwardedRef} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
@@ -23,15 +24,28 @@ type CountrySelectorProps = {
/** inputID used by the Form component */
// eslint-disable-next-line react/no-unused-prop-types
inputID: string;
+
+ /** Callback to call when the picker modal is dismissed */
+ onBlur?: () => void;
};
-function CountrySelector({errorText = '', value: countryCode, onInputChange}: CountrySelectorProps, ref: ForwardedRef) {
+function CountrySelector({errorText = '', value: countryCode, onInputChange, onBlur}: CountrySelectorProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const title = countryCode ? translate(`allCountries.${countryCode}`) : '';
const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null;
+ const didOpenContrySelector = useRef(false);
+ const isFocused = useIsFocused();
+ useEffect(() => {
+ if (!isFocused || !didOpenContrySelector.current) {
+ return;
+ }
+ didOpenContrySelector.current = false;
+ onBlur?.();
+ }, [isFocused, onBlur]);
+
useEffect(() => {
// This will cause the form to revalidate and remove any error related to country name
onInputChange(countryCode);
@@ -48,6 +62,7 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange}: Co
description={translate('common.country')}
onPress={() => {
const activeRoute = Navigation.getActiveRouteWithoutParams();
+ didOpenContrySelector.current = true;
Navigation.navigate(ROUTES.SETTINGS_ADDRESS_COUNTRY.getRoute(countryCode ?? '', activeRoute));
}}
/>
diff --git a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx
index 49915ebfbf1b..f8c4a12ec188 100644
--- a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx
+++ b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx
@@ -3,6 +3,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
+import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
@@ -79,6 +80,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear
showScrollIndicator
shouldStopPropagation
shouldUseDynamicMaxToRenderPerBatch
+ ListItem={RadioListItem}
/>
diff --git a/src/components/DistanceRequest/index.tsx b/src/components/DistanceRequest/index.tsx
index 29987f716565..3f74c148de70 100644
--- a/src/components/DistanceRequest/index.tsx
+++ b/src/components/DistanceRequest/index.tsx
@@ -28,6 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Report, Transaction} from '@src/types/onyx';
import type {WaypointCollection} from '@src/types/onyx/Transaction';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import DistanceRequestFooter from './DistanceRequestFooter';
import DistanceRequestRenderItem from './DistanceRequestRenderItem';
@@ -176,7 +177,7 @@ function DistanceRequest({transactionID = '', report, transaction, route, isEdit
);
};
- const getError = () => {
+ const getError = useCallback(() => {
// Get route error if available else show the invalid number of waypoints error.
if (hasRouteError) {
return ErrorUtils.getLatestErrorField((transaction ?? {}) as Transaction, 'route');
@@ -186,8 +187,12 @@ function DistanceRequest({transactionID = '', report, transaction, route, isEdit
// eslint-disable-next-line @typescript-eslint/naming-convention
return {0: 'iou.error.atLeastTwoDifferentWaypoints'};
}
- return {};
- };
+
+ if (Object.keys(validatedWaypoints).length < Object.keys(waypoints).length) {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ return {0: translate('iou.error.duplicateWaypointsErrorMessage')};
+ }
+ }, [translate, transaction, hasRouteError, validatedWaypoints, waypoints]);
const updateWaypoints = useCallback(
({data}: DraggableListData) => {
@@ -211,7 +216,7 @@ function DistanceRequest({transactionID = '', report, transaction, route, isEdit
const submitWaypoints = useCallback(() => {
// If there is any error or loading state, don't let user go to next page.
- if (Object.keys(validatedWaypoints).length < 2 || hasRouteError || isLoadingRoute || (isLoading && !isOffline)) {
+ if (!isEmptyObject(getError()) || isLoadingRoute || (isLoading && !isOffline)) {
setHasError(true);
return;
}
@@ -221,7 +226,7 @@ function DistanceRequest({transactionID = '', report, transaction, route, isEdit
}
onSubmit(waypoints);
- }, [onSubmit, setHasError, hasRouteError, isLoadingRoute, isLoading, validatedWaypoints, waypoints, isEditingNewRequest, isEditingRequest, isOffline]);
+ }, [onSubmit, setHasError, getError, isLoadingRoute, isLoading, waypoints, isEditingNewRequest, isEditingRequest, isOffline]);
const content = (
<>
@@ -254,10 +259,10 @@ function DistanceRequest({transactionID = '', report, transaction, route, isEdit
{/* Show error message if there is route error or there are less than 2 routes and user has tried submitting, */}
- {((hasError && Object.keys(validatedWaypoints).length < 2) || hasRouteError) && (
+ {((hasError && !isEmptyObject(getError())) || hasRouteError) && (
)}
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
index 863fe6fbabb1..465a4f747bcb 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
@@ -65,7 +65,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) {
// eslint-disable-next-line react/jsx-props-no-multi-spaces
target={htmlAttribs.target || '_blank'}
rel={htmlAttribs.rel || 'noopener noreferrer'}
- style={[parentStyle, styles.textUnderlinePositionUnder, styles.textDecorationSkipInkNone, style]}
+ style={[style, parentStyle, styles.textUnderlinePositionUnder, styles.textDecorationSkipInkNone]}
key={key}
// Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling
onPress={internalNewExpensifyPath || internalExpensifyPath ? () => Link.openLink(attrHref, environmentURL, isAttachment) : undefined}
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
index 8e0ce759d021..f2e38ccb74af 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx
@@ -10,7 +10,6 @@ import Text from '@components/Text';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
-import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
@@ -27,7 +26,6 @@ type MentionUserRendererProps = WithCurrentUserPersonalDetailsProps & CustomRend
function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersonalDetails, ...defaultRendererProps}: MentionUserRendererProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const {translate} = useLocalize();
const htmlAttribAccountID = tnode.attributes.accountid;
const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
@@ -39,7 +37,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona
const user = personalDetails[htmlAttribAccountID];
accountID = parseInt(htmlAttribAccountID, 10);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(user?.login ?? '') || user?.displayName || translate('common.hidden');
+ displayNameOrLogin = PersonalDetailsUtils.getDisplayNameOrDefault(user, LocalePhoneNumber.formatPhoneNumber(user?.login ?? ''));
navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID);
} else if ('data' in tnode && !isEmptyObject(tnode.data)) {
// We need to remove the LTR unicode and leading @ from data as it is not part of the login
diff --git a/src/components/Image/index.js b/src/components/Image/index.js
index ef1a69e19c12..59fcde8273fd 100644
--- a/src/components/Image/index.js
+++ b/src/components/Image/index.js
@@ -3,12 +3,15 @@ import React, {useEffect, useMemo} from 'react';
import {Image as RNImage} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
+import useNetwork from '@hooks/useNetwork';
import ONYXKEYS from '@src/ONYXKEYS';
import {defaultProps, imagePropTypes} from './imagePropTypes';
import RESIZE_MODES from './resizeModes';
function Image(props) {
const {source: propsSource, isAuthTokenRequired, onLoad, session} = props;
+ const {isOffline} = useNetwork();
+
/**
* Check if the image source is a URL - if so the `encryptedAuthToken` is appended
* to the source.
@@ -39,7 +42,7 @@ function Image(props) {
RNImage.getSize(source.uri, (width, height) => {
onLoad({nativeEvent: {width, height}});
});
- }, [onLoad, source]);
+ }, [onLoad, source, isOffline]);
// Omit the props which the underlying RNImage won't use
const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']);
diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx
index b3fc1dc91c16..0ca4a0456e33 100644
--- a/src/components/ImageWithSizeCalculation.tsx
+++ b/src/components/ImageWithSizeCalculation.tsx
@@ -2,6 +2,7 @@ import delay from 'lodash/delay';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
+import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import Log from '@libs/Log';
import FullscreenLoadingIndicator from './FullscreenLoadingIndicator';
@@ -44,16 +45,27 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT
const isLoadedRef = useRef(null);
const [isImageCached, setIsImageCached] = useState(true);
const [isLoading, setIsLoading] = useState(false);
+ const {isOffline} = useNetwork();
const source = useMemo(() => ({uri: url}), [url]);
const onError = () => {
Log.hmmm('Unable to fetch image to calculate size', {url});
onLoadFailure?.();
+ if (isLoadedRef.current) {
+ isLoadedRef.current = false;
+ setIsImageCached(false);
+ }
+ if (isOffline) {
+ return;
+ }
+ setIsLoading(false);
};
const imageLoadedSuccessfully = (event: OnLoadNativeEvent) => {
isLoadedRef.current = true;
+ setIsLoading(false);
+ setIsImageCached(true);
onMeasure({
width: event.nativeEvent.width,
height: event.nativeEvent.height,
@@ -87,10 +99,6 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT
}
setIsLoading(true);
}}
- onLoadEnd={() => {
- setIsLoading(false);
- setIsImageCached(true);
- }}
onError={onError}
onLoad={imageLoadedSuccessfully}
/>
diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx
index a3178f642852..77447f13644c 100644
--- a/src/components/MapView/MapView.tsx
+++ b/src/components/MapView/MapView.tsx
@@ -30,7 +30,21 @@ const MapView = forwardRef(
const [isIdle, setIsIdle] = useState(false);
const [currentPosition, setCurrentPosition] = useState(cachedUserLocation);
const [userInteractedWithMap, setUserInteractedWithMap] = useState(false);
- const hasAskedForLocationPermission = useRef(false);
+ const shouldInitializeCurrentPosition = useRef(true);
+
+ // Determines if map can be panned to user's detected
+ // location without bothering the user. It will return
+ // false if user has already started dragging the map or
+ // if there are one or more waypoints present.
+ const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]);
+
+ const setCurrentPositionToInitialState = useCallback(() => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (cachedUserLocation || !initialState) {
+ return;
+ }
+ setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]});
+ }, [initialState, cachedUserLocation]);
useFocusEffect(
useCallback(() => {
@@ -38,34 +52,24 @@ const MapView = forwardRef(
return;
}
- if (hasAskedForLocationPermission.current) {
+ if (!shouldInitializeCurrentPosition.current) {
return;
}
- hasAskedForLocationPermission.current = true;
- getCurrentPosition(
- (params) => {
- const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude};
- setCurrentPosition(currentCoords);
- setUserLocation(currentCoords);
- },
- () => {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- if (cachedUserLocation || !initialState) {
- return;
- }
-
- setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]});
- },
- );
- }, [cachedUserLocation, initialState, isOffline]),
- );
+ shouldInitializeCurrentPosition.current = false;
- // Determines if map can be panned to user's detected
- // location without bothering the user. It will return
- // false if user has already started dragging the map or
- // if there are one or more waypoints present.
- const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]);
+ if (!shouldPanMapToCurrentPosition()) {
+ setCurrentPositionToInitialState();
+ return;
+ }
+
+ getCurrentPosition((params) => {
+ const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude};
+ setCurrentPosition(currentCoords);
+ setUserLocation(currentCoords);
+ }, setCurrentPositionToInitialState);
+ }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]),
+ );
useEffect(() => {
if (!currentPosition || !cameraRef.current) {
diff --git a/src/components/MapView/MapView.website.tsx b/src/components/MapView/MapView.website.tsx
index 289f7d0d62a8..05be6d6409e8 100644
--- a/src/components/MapView/MapView.website.tsx
+++ b/src/components/MapView/MapView.website.tsx
@@ -53,7 +53,21 @@ const MapView = forwardRef(
const [userInteractedWithMap, setUserInteractedWithMap] = useState(false);
const [shouldResetBoundaries, setShouldResetBoundaries] = useState(false);
const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []);
- const hasAskedForLocationPermission = useRef(false);
+ const shouldInitializeCurrentPosition = useRef(true);
+
+ // Determines if map can be panned to user's detected
+ // location without bothering the user. It will return
+ // false if user has already started dragging the map or
+ // if there are one or more waypoints present.
+ const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]);
+
+ const setCurrentPositionToInitialState = useCallback(() => {
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ if (cachedUserLocation || !initialState) {
+ return;
+ }
+ setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]});
+ }, [initialState, cachedUserLocation]);
useFocusEffect(
useCallback(() => {
@@ -61,34 +75,24 @@ const MapView = forwardRef(
return;
}
- if (hasAskedForLocationPermission.current) {
+ if (!shouldInitializeCurrentPosition.current) {
return;
}
- hasAskedForLocationPermission.current = true;
- getCurrentPosition(
- (params) => {
- const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude};
- setCurrentPosition(currentCoords);
- setUserLocation(currentCoords);
- },
- () => {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- if (cachedUserLocation || !initialState) {
- return;
- }
-
- setCurrentPosition({longitude: initialState.location[0], latitude: initialState.location[1]});
- },
- );
- }, [cachedUserLocation, initialState, isOffline]),
- );
+ shouldInitializeCurrentPosition.current = false;
- // Determines if map can be panned to user's detected
- // location without bothering the user. It will return
- // false if user has already started dragging the map or
- // if there are one or more waypoints present.
- const shouldPanMapToCurrentPosition = useCallback(() => !userInteractedWithMap && (!waypoints || waypoints.length === 0), [userInteractedWithMap, waypoints]);
+ if (!shouldPanMapToCurrentPosition()) {
+ setCurrentPositionToInitialState();
+ return;
+ }
+
+ getCurrentPosition((params) => {
+ const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude};
+ setCurrentPosition(currentCoords);
+ setUserLocation(currentCoords);
+ }, setCurrentPositionToInitialState);
+ }, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]),
+ );
useEffect(() => {
if (!currentPosition || !mapRef) {
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 8fc9c62bfb38..1c2a8a3197fe 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -573,10 +573,12 @@ function MenuItem(
{badgeText && (
diff --git a/src/components/QRShare/index.tsx b/src/components/QRShare/index.tsx
index 45a4a4fd4964..c7e9e7637a6c 100644
--- a/src/components/QRShare/index.tsx
+++ b/src/components/QRShare/index.tsx
@@ -9,15 +9,12 @@ import QRCode from '@components/QRCode';
import Text from '@components/Text';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import useWindowDimensions from '@hooks/useWindowDimensions';
import variables from '@styles/variables';
-import CONST from '@src/CONST';
import type {QRShareHandle, QRShareProps} from './types';
function QRShare({url, title, subtitle, logo, logoRatio, logoMarginRatio}: QRShareProps, ref: ForwardedRef) {
const styles = useThemeStyles();
const theme = useTheme();
- const {isSmallScreenWidth} = useWindowDimensions();
const [qrCodeSize, setQrCodeSize] = useState(1);
const svgRef = useRef
)}
-
-
-
-
-
+
+ {({anchor, report, action, checkIfContextMenuActive}) => (
+ DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
+ onPressOut={() => ControlSelection.unblock()}
+ onLongPress={(event) =>
+ showContextMenuForReport(event, anchor, (report && report.reportID) || '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))
+ }
+ >
+
+
+
+
+ )}
+
);
}
diff --git a/src/components/__mocks__/ConfirmedRoute.tsx b/src/components/__mocks__/ConfirmedRoute.tsx
new file mode 100644
index 000000000000..3c78e764ebea
--- /dev/null
+++ b/src/components/__mocks__/ConfirmedRoute.tsx
@@ -0,0 +1,8 @@
+import {View} from 'react-native';
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
+function ConfirmedRoute(props: any) {
+ return ;
+}
+
+export default ConfirmedRoute;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index da7a1d0b7586..0553d6470ddc 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -26,6 +26,7 @@ import type {
FormattedMaxLengthParams,
GoBackMessageParams,
GoToRoomParams,
+ HeldRequestParams,
InstantSummaryParams,
LocalTimeParams,
LoggedInAsParams,
@@ -666,8 +667,10 @@ export default {
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`,
enableWallet: 'Enable Wallet',
hold: 'Hold',
- holdRequest: 'Hold Request',
- unholdRequest: 'Unhold Request',
+ holdRequest: 'Hold request',
+ unholdRequest: 'Unhold request',
+ heldRequest: ({comment}: HeldRequestParams) => `held this request with the comment: ${comment}`,
+ unheldRequest: 'unheld this request',
explainHold: "Explain why you're holding this request.",
reason: 'Reason',
holdReasonRequired: 'A reason is required when holding.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 23cf8be8c30c..2a2eb96bd488 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -25,6 +25,7 @@ import type {
FormattedMaxLengthParams,
GoBackMessageParams,
GoToRoomParams,
+ HeldRequestParams,
InstantSummaryParams,
LocalTimeParams,
LoggedInAsParams,
@@ -660,8 +661,10 @@ export default {
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `IniciΓ³ el pago, pero no se procesarΓ‘ hasta que ${submitterDisplayName} active su Billetera`,
enableWallet: 'Habilitar Billetera',
- holdRequest: 'Bloquear solicitud de dinero',
- unholdRequest: 'Desbloquear solicitud de dinero',
+ holdRequest: 'Bloquear solicitud',
+ unholdRequest: 'Desbloquear solicitud',
+ heldRequest: ({comment}: HeldRequestParams) => `bloqueΓ³ esta solicitud con el comentario: ${comment}`,
+ unheldRequest: 'desbloqueΓ³ esta solicitud',
explainHold: 'Explica la razΓ³n para bloquear esta solicitud.',
reason: 'RazΓ³n',
holdReasonRequired: 'Se requiere una razΓ³n para bloquear.',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index f7e580819fdf..410c8e1c2085 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -105,7 +105,7 @@ type SettleExpensifyCardParams = {
formattedAmount: string;
};
-type RequestAmountParams = {amount: number};
+type RequestAmountParams = {amount: string};
type RequestedAmountMessageParams = {formattedAmount: string; comment?: string};
@@ -293,6 +293,8 @@ type ElectronicFundsParams = {percentage: string; amount: string};
type LogSizeParams = {size: number};
+type HeldRequestParams = {comment: string};
+
export type {
AdminCanceledRequestParams,
ApprovedAmountParams,
@@ -395,4 +397,5 @@ export type {
WelcomeToRoomParams,
ZipCodeExampleFormatParams,
LogSizeParams,
+ HeldRequestParams,
};
diff --git a/src/libs/API/parameters/UpdateRoomVisibilityParams.ts b/src/libs/API/parameters/UpdateRoomVisibilityParams.ts
new file mode 100644
index 000000000000..a69559f0ce47
--- /dev/null
+++ b/src/libs/API/parameters/UpdateRoomVisibilityParams.ts
@@ -0,0 +1,8 @@
+import type {RoomVisibility} from '@src/types/onyx/Report';
+
+type UpdateRoomVisibilityParams = {
+ reportID: string;
+ visibility: RoomVisibility;
+};
+
+export default UpdateRoomVisibilityParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 371fb8ddb404..2633d795b561 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -84,6 +84,7 @@ export type {default as DeleteCommentParams} from './DeleteCommentParams';
export type {default as UpdateCommentParams} from './UpdateCommentParams';
export type {default as UpdateReportNotificationPreferenceParams} from './UpdateReportNotificationPreferenceParams';
export type {default as UpdateRoomDescriptionParams} from './UpdateRoomDescriptionParams';
+export type {default as UpdateRoomVisibilityParams} from './UpdateRoomVisibilityParams';
export type {default as UpdateReportWriteCapabilityParams} from './UpdateReportWriteCapabilityParams';
export type {default as AddWorkspaceRoomParams} from './AddWorkspaceRoomParams';
export type {default as UpdatePolicyRoomNameParams} from './UpdatePolicyRoomNameParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 9c0d57b1cf14..35b03f21c841 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -85,6 +85,7 @@ const WRITE_COMMANDS = {
DELETE_COMMENT: 'DeleteComment',
UPDATE_COMMENT: 'UpdateComment',
UPDATE_REPORT_NOTIFICATION_PREFERENCE: 'UpdateReportNotificationPreference',
+ UPDATE_ROOM_VISIBILITY: 'UpdateRoomVisibility',
UPDATE_ROOM_DESCRIPTION: 'UpdateRoomDescription',
UPDATE_REPORT_WRITE_CAPABILITY: 'UpdateReportWriteCapability',
ADD_WORKSPACE_ROOM: 'AddWorkspaceRoom',
@@ -226,6 +227,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.DELETE_COMMENT]: Parameters.DeleteCommentParams;
[WRITE_COMMANDS.UPDATE_COMMENT]: Parameters.UpdateCommentParams;
[WRITE_COMMANDS.UPDATE_REPORT_NOTIFICATION_PREFERENCE]: Parameters.UpdateReportNotificationPreferenceParams;
+ [WRITE_COMMANDS.UPDATE_ROOM_VISIBILITY]: Parameters.UpdateRoomVisibilityParams;
[WRITE_COMMANDS.UPDATE_ROOM_DESCRIPTION]: Parameters.UpdateRoomDescriptionParams;
[WRITE_COMMANDS.UPDATE_REPORT_WRITE_CAPABILITY]: Parameters.UpdateReportWriteCapabilityParams;
[WRITE_COMMANDS.ADD_WORKSPACE_ROOM]: Parameters.AddWorkspaceRoomParams;
diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts
index cf49ba03f287..24437da48953 100644
--- a/src/libs/GetPhysicalCardUtils.ts
+++ b/src/libs/GetPhysicalCardUtils.ts
@@ -3,17 +3,18 @@ import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type {GetPhysicalCardForm} from '@src/types/form';
import type {LoginList, PrivatePersonalDetails} from '@src/types/onyx';
+import * as LoginUtils from './LoginUtils';
import Navigation from './Navigation/Navigation';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as UserUtils from './UserUtils';
-function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): Route {
+function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry): Route {
const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {};
if (!legalFirstName && !legalLastName) {
return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain);
}
- if (!phoneNumber && !UserUtils.getSecondaryPhoneLogin(loginList)) {
+ if (!phoneNumber || !LoginUtils.validateNumber(phoneNumber)) {
return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain);
}
if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) {
@@ -23,8 +24,8 @@ function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) {
- Navigation.navigate(getCurrentRoute(domain, privatePersonalDetails, loginList));
+function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: OnyxEntry) {
+ Navigation.navigate(getCurrentRoute(domain, privatePersonalDetails));
}
/**
@@ -35,8 +36,8 @@ function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: OnyxE
* @param loginList
* @returns
*/
-function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) {
- const expectedRoute = getCurrentRoute(domain, privatePersonalDetails, loginList);
+function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDetails: OnyxEntry) {
+ const expectedRoute = getCurrentRoute(domain, privatePersonalDetails);
// If the user is on the current route or the current route is confirmation, then he's allowed to stay on the current step
if ([currentRoute, ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_CONFIRM.getRoute(domain)].includes(expectedRoute)) {
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index d9835b01ceff..3af123a74910 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -133,6 +133,7 @@ const ReportSettingsModalStackNavigator = createModalStackNavigator require('../../../pages/settings/Report/RoomNamePage').default as React.ComponentType,
[SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: () => require('../../../pages/settings/Report/NotificationPreferencePage').default as React.ComponentType,
[SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: () => require('../../../pages/settings/Report/WriteCapabilityPage').default as React.ComponentType,
+ [SCREENS.REPORT_SETTINGS.VISIBILITY]: () => require('../../../pages/settings/Report/VisibilityPage').default as React.ComponentType,
});
const TaskModalStackNavigator = createModalStackNavigator({
@@ -191,7 +192,6 @@ const AccountSettingsModalStackNavigator = createModalStackNavigator(
[SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require('../../../pages/settings/Preferences/PreferencesPage').default as React.ComponentType,
[SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../pages/settings/Profile/ProfilePage').default as React.ComponentType,
- [SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../pages/ShareCodePage').default as React.ComponentType,
[SCREENS.SETTINGS.WALLET.ROOT]: () => require('../../../pages/settings/Wallet/WalletPage').default as React.ComponentType,
[SCREENS.SETTINGS.ABOUT]: () => require('../../../pages/settings/AboutPage/AboutPage').default as React.ComponentType,
},
@@ -203,6 +203,7 @@ const WorkspaceSwitcherModalStackNavigator = createModalStackNavigator({
+ [SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../pages/ShareCodePage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.PRONOUNS]: () => require('../../../pages/settings/Profile/PronounsPage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.DISPLAY_NAME]: () => require('../../../pages/settings/Profile/DisplayNamePage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.TIMEZONE]: () => require('../../../pages/settings/Profile/TimezoneInitialPage').default as React.ComponentType,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 74a00dec0a1f..2640025efa09 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -261,6 +261,9 @@ const config: LinkingOptions['config'] = {
path: ROUTES.KEYBOARD_SHORTCUTS,
},
[SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route,
+ [SCREENS.SETTINGS.SHARE_CODE]: {
+ path: ROUTES.SETTINGS_SHARE_CODE,
+ },
},
},
[SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: {
@@ -289,6 +292,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: {
path: ROUTES.REPORT_SETTINGS_WRITE_CAPABILITY.route,
},
+ [SCREENS.REPORT_SETTINGS.VISIBILITY]: {
+ path: ROUTES.REPORT_SETTINGS_VISIBILITY.route,
+ },
},
},
[SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: {
@@ -495,10 +501,6 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.SETTINGS_CENTRAL_PANE]: {
screens: {
- [SCREENS.SETTINGS.SHARE_CODE]: {
- path: ROUTES.SETTINGS_SHARE_CODE,
- exact: true,
- },
[SCREENS.SETTINGS.PROFILE.ROOT]: {
path: ROUTES.SETTINGS_PROFILE,
exact: true,
diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index 8e246d82ff72..e7c5466852cf 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -70,14 +70,16 @@ function createCentralPaneNavigator(route: NavigationPartialRoute): NavigationPartialRoute {
+function createFullScreenNavigator(route?: NavigationPartialRoute): NavigationPartialRoute {
const routes = [];
routes.push({name: SCREENS.SETTINGS.ROOT});
- routes.push({
- name: SCREENS.SETTINGS_CENTRAL_PANE,
- state: getRoutesWithIndex([route]),
- });
+ if (route) {
+ routes.push({
+ name: SCREENS.SETTINGS_CENTRAL_PANE,
+ state: getRoutesWithIndex([route]),
+ });
+ }
return {
name: NAVIGATORS.FULL_SCREEN_NAVIGATOR,
@@ -129,6 +131,11 @@ function getMatchingRootRouteForRHPRoute(
return createFullScreenNavigator({name: fullScreenName as FullScreenName, params: route.params});
}
}
+
+ // This screen is opened from the LHN of the FullStackNavigator, so in this case we shouldn't push any CentralPane screen
+ if (route.name === SCREENS.SETTINGS.SHARE_CODE) {
+ return createFullScreenNavigator();
+ }
}
function getAdaptedState(state: PartialState>, policyID?: string): GetAdaptedStateReturnType {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 04bc25736887..81229f353e52 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -203,6 +203,9 @@ type ReportSettingsNavigatorParamList = {
[SCREENS.REPORT_SETTINGS.ROOM_NAME]: undefined;
[SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: undefined;
[SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: undefined;
+ [SCREENS.REPORT_SETTINGS.VISIBILITY]: {
+ reportID: string;
+ };
};
type ReportDescriptionNavigatorParamList = {
@@ -416,6 +419,7 @@ type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.NEW_CHAT]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams;
+ [SCREENS.SETTINGS.SHARE_CODE]: undefined;
[SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: NavigatorScreenParams;
@@ -440,7 +444,6 @@ type RightModalNavigatorParamList = {
};
type SettingsCentralPaneNavigatorParamList = {
- [SCREENS.SETTINGS.SHARE_CODE]: undefined;
[SCREENS.SETTINGS.PROFILE.ROOT]: undefined;
[SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined;
[SCREENS.SETTINGS.SECURITY]: undefined;
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 80081061f340..97b4fc0144c8 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -1494,6 +1494,10 @@ function getOptions(
return;
}
+ if (!accountIDs || accountIDs.length === 0) {
+ return;
+ }
+
// Save the report in the map if this is a single participant so we can associate the reportID with the
// personal detail option later. Individuals should not be associated with single participant
// policyExpenseChats or chatRooms since those are not people.
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 6abdd4348488..d9e7fb8e7e6b 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -62,6 +62,7 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PolicyUtils from './PolicyUtils';
import type {LastVisibleMessage} from './ReportActionsUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
+import shouldAllowRawHTMLMessages from './shouldAllowRawHTMLMessages';
import * as TransactionUtils from './TransactionUtils';
import * as Url from './Url';
import * as UserUtils from './UserUtils';
@@ -2305,7 +2306,6 @@ function getReportPreviewMessage(
isPreviewMessageForParentChatReport = false,
policy: OnyxEntry = null,
isForListPreview = false,
- shouldHidePayer = false,
): string {
const reportActionMessage = reportAction?.message?.[0].html ?? '';
@@ -2370,9 +2370,7 @@ function getReportPreviewMessage(
if (isSettled(report.reportID) || (report.isWaitingOnBankAccount && isPreviewMessageForParentChatReport)) {
// A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify"
let translatePhraseKey: TranslationPaths = 'iou.paidElsewhereWithAmount';
- if (isPreviewMessageForParentChatReport) {
- translatePhraseKey = 'iou.payerPaidAmount';
- } else if (
+ if (
[CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].some((paymentType) => paymentType === originalMessage?.paymentType) ||
!!reportActionMessage.match(/ (with Expensify|using Expensify)$/) ||
report.isWaitingOnBankAccount
@@ -2380,7 +2378,7 @@ function getReportPreviewMessage(
translatePhraseKey = 'iou.paidWithExpensifyWithAmount';
}
- let actualPayerName = report.managerID === currentUserAccountID || shouldHidePayer ? '' : getDisplayNameForParticipant(report.managerID, true);
+ let actualPayerName = report.managerID === currentUserAccountID ? '' : getDisplayNameForParticipant(report.managerID, true);
actualPayerName = actualPayerName && isForListPreview && !isPreviewMessageForParentChatReport ? `${actualPayerName}:` : actualPayerName;
const payerDisplayName = isPreviewMessageForParentChatReport ? payerName : actualPayerName;
@@ -2697,7 +2695,7 @@ function hasReportNameError(report: OnyxEntry): boolean {
*/
function getParsedComment(text: string): string {
const parser = new ExpensiMark();
- return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text);
+ return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text, {shouldEscapeText: !shouldAllowRawHTMLMessages()}) : lodashEscape(text);
}
function getReportDescriptionText(report: Report): string {
@@ -3529,7 +3527,7 @@ function buildOptimisticHoldReportAction(comment: string, created = DateUtils.ge
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'normal',
- text: `held this money request with the comment: ${comment}`,
+ text: Localize.translateLocal('iou.heldRequest', {comment}),
},
{
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
@@ -3564,7 +3562,7 @@ function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): Opt
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'normal',
- text: `unheld this money request`,
+ text: Localize.translateLocal('iou.unheldRequest'),
},
],
person: [
@@ -4527,6 +4525,13 @@ function canEditWriteCapability(report: OnyxEntry, policy: OnyxEntry, policy: OnyxEntry): boolean {
+ return PolicyUtils.isPolicyAdmin(policy) && !isArchivedRoom(report);
+}
+
/**
* Returns the onyx data needed for the task assignee chat
*/
@@ -4689,6 +4694,62 @@ function getVisibleMemberIDs(report: OnyxEntry): number[] {
return visibleChatMemberAccountIDs;
}
+/**
+ * Return iou report action display message
+ */
+function getIOUReportActionDisplayMessage(reportAction: OnyxEntry): string {
+ if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) {
+ return '';
+ }
+ const originalMessage = reportAction.originalMessage;
+ const {IOUReportID} = originalMessage;
+ const iouReport = getReport(IOUReportID);
+ let translationKey: TranslationPaths;
+ if (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY) {
+ // The `REPORT_ACTION_TYPE.PAY` action type is used for both fulfilling existing requests and sending money. To
+ // differentiate between these two scenarios, we check if the `originalMessage` contains the `IOUDetails`
+ // property. If it does, it indicates that this is a 'Send money' action.
+ const {amount, currency} = originalMessage.IOUDetails ?? originalMessage;
+ const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(amount), currency) ?? '';
+ const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true);
+
+ switch (originalMessage.paymentType) {
+ case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
+ translationKey = 'iou.paidElsewhereWithAmount';
+ break;
+ case CONST.IOU.PAYMENT_TYPE.EXPENSIFY:
+ case CONST.IOU.PAYMENT_TYPE.VBBA:
+ translationKey = 'iou.paidWithExpensifyWithAmount';
+ break;
+ default:
+ translationKey = 'iou.payerPaidAmount';
+ break;
+ }
+ return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName ?? ''});
+ }
+
+ const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? '');
+ const transactionDetails = getTransactionDetails(!isEmptyObject(transaction) ? transaction : null);
+ const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency);
+ const isRequestSettled = isSettled(originalMessage.IOUReportID);
+ const isApproved = isReportApproved(iouReport);
+ if (isRequestSettled) {
+ return Localize.translateLocal('iou.payerSettled', {
+ amount: formattedAmount,
+ });
+ }
+ if (isApproved) {
+ return Localize.translateLocal('iou.approvedAmount', {
+ amount: formattedAmount,
+ });
+ }
+ translationKey = ReportActionsUtils.isSplitBillAction(reportAction) ? 'iou.didSplitAmount' : 'iou.requestedAmount';
+ return Localize.translateLocal(translationKey, {
+ formattedAmount,
+ comment: transactionDetails?.comment ?? '',
+ });
+}
+
/**
* Checks if a report is a group chat.
*
@@ -5126,6 +5187,7 @@ export {
hasOnlyTransactionsWithPendingRoutes,
hasNonReimbursableTransactions,
hasMissingSmartscanFields,
+ getIOUReportActionDisplayMessage,
isWaitingForAssigneeToCompleteTask,
isGroupChat,
isDraftExpenseReport,
@@ -5160,6 +5222,7 @@ export {
getAvailableReportFields,
reportFieldsEnabled,
getAllAncestorReportActionIDs,
+ canEditRoomVisibility,
canEditPolicyDescription,
getPolicyDescriptionText,
};
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index d3eafc6554db..0a13d561891c 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -6,8 +6,9 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {RecentWaypoint, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx';
import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates';
-import type {Comment, Receipt, TransactionChanges, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
+import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {isCorporateCard, isExpensifyCard} from './CardUtils';
import DateUtils from './DateUtils';
import * as NumberUtils from './NumberUtils';
@@ -94,6 +95,7 @@ function buildOptimisticTransaction(
category = '',
tag = '',
billable = false,
+ pendingFields: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}> | undefined = undefined,
): Transaction {
// transactionIDs are random, positive, 64-bit numeric strings.
// Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID)
@@ -108,6 +110,7 @@ function buildOptimisticTransaction(
}
return {
+ ...(!isEmptyObject(pendingFields) ? {pendingFields} : {}),
transactionID,
amount,
currency,
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 47d10ddcef4b..39ce9dd6d2bb 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -223,8 +223,7 @@ Onyx.connect({
* @param reportID to attach the transaction to
* @param iouRequestType one of manual/scan/distance
*/
-// eslint-disable-next-line @typescript-eslint/naming-convention
-function startMoneyRequest_temporaryForRefactor(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) {
+function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) {
// Generate a brand new transactionID
const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID;
// Disabling this line since currentDate can be an empty string
@@ -259,6 +258,12 @@ function clearMoneyRequest(transactionID: string) {
Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, null);
}
+// eslint-disable-next-line @typescript-eslint/naming-convention
+function startMoneyRequest_temporaryForRefactor(iouType: ValueOf, reportID: string) {
+ clearMoneyRequest(CONST.IOU.OPTIMISTIC_TRANSACTION_ID);
+ Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, reportID));
+}
+
// eslint-disable-next-line @typescript-eslint/naming-convention
function setMoneyRequestAmount_temporaryForRefactor(transactionID: string, amount: number, currency: string, removeOriginalCurrency = false) {
if (removeOriginalCurrency) {
@@ -789,6 +794,8 @@ function getMoneyRequestInformation(
receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY;
filename = receipt.name;
}
+ const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`];
+ const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE;
let optimisticTransaction = TransactionUtils.buildOptimisticTransaction(
ReportUtils.isExpenseReport(iouReport) ? -amount : amount,
currency,
@@ -804,6 +811,7 @@ function getMoneyRequestInformation(
category,
tag,
billable,
+ isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined,
);
const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category);
@@ -814,8 +822,7 @@ function getMoneyRequestInformation(
// data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109.
// I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417
// to remind me to do this.
- const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`];
- if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) {
+ if (isDistanceRequest) {
optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction, false);
}
@@ -3293,9 +3300,10 @@ function getSendMoneyParams(
}
function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType): PayMoneyRequestData {
+ const total = iouReport.total ?? 0;
const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction(
CONST.IOU.REPORT_ACTION_TYPE.PAY,
- -(iouReport.total ?? 0),
+ ReportUtils.isExpenseReport(iouReport) ? -total : total,
iouReport.currency ?? '',
'',
[recipient],
@@ -4095,11 +4103,11 @@ export {
payMoneyRequest,
sendMoneyWithWallet,
startMoneyRequest,
+ initMoneyRequest,
startMoneyRequest_temporaryForRefactor,
resetMoneyRequestCategory,
resetMoneyRequestCategory_temporaryForRefactor,
resetMoneyRequestInfo,
- clearMoneyRequest,
setMoneyRequestAmount_temporaryForRefactor,
setMoneyRequestBillable_temporaryForRefactor,
setMoneyRequestCategory_temporaryForRefactor,
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 9134f0a89e61..6efe0860e9b5 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -40,6 +40,7 @@ import type {
UpdateReportWriteCapabilityParams,
UpdateRoomDescriptionParams,
} from '@libs/API/parameters';
+import type UpdateRoomVisibilityParams from '@libs/API/parameters/UpdateRoomVisibilityParams';
import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as CollectionUtils from '@libs/CollectionUtils';
import DateUtils from '@libs/DateUtils';
@@ -68,7 +69,7 @@ import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx';
import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage';
-import type {NotificationPreference, WriteCapability} from '@src/types/onyx/Report';
+import type {NotificationPreference, RoomVisibility, WriteCapability} from '@src/types/onyx/Report';
import type Report from '@src/types/onyx/Report';
import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction';
import type ReportAction from '@src/types/onyx/ReportAction';
@@ -1442,6 +1443,38 @@ function updateNotificationPreference(
}
}
+function updateRoomVisibility(reportID: string, previousValue: RoomVisibility | undefined, newValue: RoomVisibility, navigate: boolean, report: OnyxEntry | EmptyObject = {}) {
+ if (previousValue === newValue) {
+ if (navigate && !isEmptyObject(report) && report.reportID) {
+ ReportUtils.goBackToDetailsPage(report);
+ }
+ return;
+ }
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {visibility: newValue},
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ value: {visibility: previousValue},
+ },
+ ];
+
+ const parameters: UpdateRoomVisibilityParams = {reportID, visibility: newValue};
+
+ API.write(WRITE_COMMANDS.UPDATE_ROOM_VISIBILITY, parameters, {optimisticData, failureData});
+ if (navigate && !isEmptyObject(report)) {
+ ReportUtils.goBackToDetailsPage(report);
+ }
+}
+
/**
* This will subscribe to an existing thread, or create a new one and then subsribe to it if necessary
*
@@ -1610,7 +1643,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre
const parameters = {
reportID,
- reportFields: JSON.stringify({[reportField.fieldID]: reportField}),
+ reportFields: JSON.stringify({[`expensify_${reportField.fieldID}`]: reportField}),
};
API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData});
@@ -2926,4 +2959,5 @@ export {
updateReportField,
updateReportName,
resolveActionableMentionWhisper,
+ updateRoomVisibility,
};
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 54efe4ba4d8e..f2507a28d576 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -526,7 +526,7 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) {
}
// mention user
- if ('html' in message && typeof message.html === 'string' && message.html.includes('')) {
+ if ('html' in message && typeof message.html === 'string' && message.html.includes(`@${currentEmail}`)) {
return playSound(SOUNDS.ATTENTION);
}
diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.ts
similarity index 71%
rename from src/libs/migrateOnyx.js
rename to src/libs/migrateOnyx.ts
index 9b8b4056e3e5..1202275067a5 100644
--- a/src/libs/migrateOnyx.js
+++ b/src/libs/migrateOnyx.ts
@@ -1,31 +1,26 @@
-import _ from 'underscore';
import Log from './Log';
import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID';
-import PersonalDetailsByAccountID from './migrations/PersonalDetailsByAccountID';
import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts';
import RenameReceiptFilename from './migrations/RenameReceiptFilename';
import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection';
-export default function () {
+export default function (): Promise {
const startTime = Date.now();
Log.info('[Migrate Onyx] start');
return new Promise((resolve) => {
// Add all migrations to an array so they are executed in order
- const migrationPromises = [PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts];
+ const migrationPromises = [RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts];
// Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the
// previous promise to finish before moving onto the next one.
/* eslint-disable arrow-body-style */
- _.reduce(
- migrationPromises,
- (previousPromise, migrationPromise) => {
+ migrationPromises
+ .reduce((previousPromise, migrationPromise) => {
return previousPromise.then(() => {
return migrationPromise();
});
- },
- Promise.resolve(),
- )
+ }, Promise.resolve())
// Once all migrations are done, resolve the main promise
.then(() => {
diff --git a/src/libs/migrations/PersonalDetailsByAccountID.js b/src/libs/migrations/PersonalDetailsByAccountID.js
deleted file mode 100644
index 24aece8f5a97..000000000000
--- a/src/libs/migrations/PersonalDetailsByAccountID.js
+++ /dev/null
@@ -1,274 +0,0 @@
-import lodashHas from 'lodash/has';
-import Onyx from 'react-native-onyx';
-import _ from 'underscore';
-import Log from '@libs/Log';
-import ONYXKEYS from '@src/ONYXKEYS';
-
-const DEPRECATED_ONYX_KEYS = {
- // Deprecated personal details object which was keyed by login instead of accountID.
- PERSONAL_DETAILS: 'personalDetails',
-};
-
-/**
- * @returns {Promise