diff --git a/.eslintrc.js b/.eslintrc.js
index ac4546567833..3c144064eb62 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -46,6 +46,7 @@ module.exports = {
touchables: ['PressableWithoutFeedback', 'PressableWithFeedback'],
},
],
+ curly: 'error',
},
},
{
diff --git a/android/app/build.gradle b/android/app/build.gradle
index b377f6930402..1e34491b04ad 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001037206
- versionName "1.3.72-6"
+ versionCode 1001037209
+ versionName "1.3.72-9"
}
flavorDimensions "default"
diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md
index ce59438a0681..b615104f6aab 100644
--- a/contributingGuides/STYLE.md
+++ b/contributingGuides/STYLE.md
@@ -567,6 +567,28 @@ A `useEffect()` that does not include referenced props or state in its dependenc
There are pros and cons of each, but ultimately we have standardized on using the `function` keyword to align things more with modern React conventions. There are also some minor cognitive overhead benefits in that you don't need to think about adding and removing brackets when encountering an implicit return. The `function` syntax also has the benefit of being able to be hoisted where arrow functions do not.
+## How do I auto-focus a TextInput using `useFocusEffect()`?
+
+```javascript
+const focusTimeoutRef = useRef(null);
+
+useFocusEffect(useCallback(() => {
+ focusTimeoutRef.current = setTimeout(() => textInputRef.current.focus(), CONST.ANIMATED_TRANSITION);
+ return () => {
+ if (!focusTimeoutRef.current) {
+ return;
+ }
+ clearTimeout(focusTimeoutRef.current);
+ };
+}, []));
+```
+
+This works better than using `onTransitionEnd` because -
+1. `onTransitionEnd` is only fired for the top card in the stack, and therefore does not fire on the new top card when popping a card off the stack. For example - pressing the back button to go from the workspace invite page to the workspace members list.
+2. Using `InteractionsManager.runAfterInteractions` with `useFocusEffect` will interrupt an in-progress transition animation.
+
+Note - This is a solution from [this PR](https://github.com/Expensify/App/pull/26415). You can find detailed discussion in comments.
+
# Onyx Best Practices
[Onyx Documentation](https://github.com/expensify/react-native-onyx)
diff --git a/docs/_sass/_search-bar.scss b/docs/_sass/_search-bar.scss
index ce085878af46..a5cc8ae2ff20 100644
--- a/docs/_sass/_search-bar.scss
+++ b/docs/_sass/_search-bar.scss
@@ -23,14 +23,25 @@ $color-gray-label: $color-gray-label;
#sidebar-search {
background-color: $color-appBG;
width: 375px;
- height: 100vh;
position: fixed;
- display: block;
+ display: flex;
+ flex-direction: column;
+ bottom: 0;
top: 0;
right: 0;
z-index: 2;
}
+#sidebar-search > div:last-child {
+ flex-grow: 1;
+ overflow-y: auto;
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}
+
@media only screen and (max-width: $breakpoint-tablet) {
#sidebar-search {
width: 100%;
@@ -156,10 +167,6 @@ label.search-label {
background-color: $color-appBG;
border: $color-appBG;
font-family: "ExpensifyNeue", "Helvetica Neue", "Helvetica", Arial, sans-serif !important;
- max-height: 80vh;
- overflow-y: scroll;
- -ms-overflow-style: none;
- scrollbar-width: none;
}
/* Hide the scrollbar */
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 3a3aa7f765a8..441bd2feab92 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.72.6
+ 1.3.72.9
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 340d56aa975c..27273c7f3866 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.72.6
+ 1.3.72.9
diff --git a/package-lock.json b/package-lock.json
index faf13c7d9d15..ff0500eb385b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.72-6",
+ "version": "1.3.72-9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.72-6",
+ "version": "1.3.72-9",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -90,7 +90,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.78",
+ "react-native-onyx": "1.0.84",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.7.1",
"react-native-performance": "^5.1.0",
@@ -41203,9 +41203,9 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.78",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.78.tgz",
- "integrity": "sha512-SxXr0AvFyXiZ4HYW4wBJA5YQgQzU4bSpLZ9ZvFhJ7Usmf65wYrVrmrJvQnMSeWJnMdyfoVGO1rLhoZHDwgqDIw==",
+ "version": "1.0.84",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.84.tgz",
+ "integrity": "sha512-qQ+o+qS5ucZLbKbG5kI0UsC42N4h1Pprg/1D7PqjDeVanS3iUv33rT4fbrHuar77g0DSTA1/M8bC2WmYrShS9A==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
@@ -77265,9 +77265,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.78",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.78.tgz",
- "integrity": "sha512-SxXr0AvFyXiZ4HYW4wBJA5YQgQzU4bSpLZ9ZvFhJ7Usmf65wYrVrmrJvQnMSeWJnMdyfoVGO1rLhoZHDwgqDIw==",
+ "version": "1.0.84",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.84.tgz",
+ "integrity": "sha512-qQ+o+qS5ucZLbKbG5kI0UsC42N4h1Pprg/1D7PqjDeVanS3iUv33rT4fbrHuar77g0DSTA1/M8bC2WmYrShS9A==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index 85c4289436fa..44b936c8c588 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.72-6",
+ "version": "1.3.72-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.",
@@ -132,7 +132,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.78",
+ "react-native-onyx": "1.0.84",
"react-native-pager-view": "^6.2.0",
"react-native-pdf": "^6.7.1",
"react-native-performance": "^5.1.0",
diff --git a/patches/react-native-image-picker+5.1.0.patch b/patches/react-native-image-picker+5.1.0.patch
new file mode 100644
index 000000000000..0defc430e669
--- /dev/null
+++ b/patches/react-native-image-picker+5.1.0.patch
@@ -0,0 +1,133 @@
+diff --git a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModuleImpl.java b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModuleImpl.java
+index 89b69a8..d86ab1e 100644
+--- a/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModuleImpl.java
++++ b/node_modules/react-native-image-picker/android/src/main/java/com/imagepicker/ImagePickerModuleImpl.java
+@@ -29,6 +29,120 @@ public class ImagePickerModuleImpl implements ActivityEventListener {
+ public static final int REQUEST_LAUNCH_VIDEO_CAPTURE = 13002;
+ public static final int REQUEST_LAUNCH_LIBRARY = 13003;
+
++ // Prevent svg images from being selected as they are not supported (Image component does not support them)
++ // and also because iOS does not allow them to be selected (for consistency).
++ // Since, we can't exclude a mime type, we instead allow all image mime types except 'image/svg+xml'.
++ // Image mime types are generated by merging the Android image mime type support and the IANA media-types lists.
++ // https://android.googlesource.com/platform/external/mime-support/+/main/mime.types#636
++ // https://www.iana.org/assignments/media-types/media-types.xhtml#image
++ private static final String[] ALLOWED_IMAGE_MIME_TYPES = {
++ "image/aces",
++ "image/apng",
++ "image/avci",
++ "image/avcs",
++ "image/avif",
++ "image/bmp",
++ "image/cgm",
++ "image/dicom-rle",
++ "image/dpx",
++ "image/emf",
++ "image/example",
++ "image/fits",
++ "image/g3fax",
++ "image/gif",
++ "image/heic-sequence",
++ "image/heic",
++ "image/heif-sequence",
++ "image/heif",
++ "image/hej2k",
++ "image/hsj2",
++ "image/ief",
++ "image/j2c",
++ "image/jls",
++ "image/jp2",
++ "image/jpeg",
++ "image/jph",
++ "image/jphc",
++ "image/jpm",
++ "image/jpx",
++ "image/jxr",
++ "image/jxrA",
++ "image/jxrS",
++ "image/jxs",
++ "image/jxsc",
++ "image/jxsi",
++ "image/jxss",
++ "image/ktx",
++ "image/ktx2",
++ "image/naplps",
++ "image/pcx",
++ "image/png",
++ "image/prs.btif",
++ "image/prs.pti",
++ "image/pwg-raster",
++ // "image/svg+xml",
++ "image/t38",
++ "image/tiff-fx",
++ "image/tiff",
++ "image/vnd.adobe.photoshop",
++ "image/vnd.airzip.accelerator.azv",
++ "image/vnd.cns.inf2",
++ "image/vnd.dece.graphic",
++ "image/vnd.djvu",
++ "image/vnd.dvb.subtitle",
++ "image/vnd.dwg",
++ "image/vnd.dxf",
++ "image/vnd.fastbidsheet",
++ "image/vnd.fpx",
++ "image/vnd.fst",
++ "image/vnd.fujixerox.edmics-mmr",
++ "image/vnd.fujixerox.edmics-rlc",
++ "image/vnd.globalgraphics.pgb",
++ "image/vnd.microsoft.icon",
++ "image/vnd.mix",
++ "image/vnd.mozilla.apng",
++ "image/vnd.ms-modi",
++ "image/vnd.net-fpx",
++ "image/vnd.pco.b16",
++ "image/vnd.radiance",
++ "image/vnd.sealed.png",
++ "image/vnd.sealedmedia.softseal.gif",
++ "image/vnd.sealedmedia.softseal.jpg",
++ "image/vnd.svf",
++ "image/vnd.tencent.tap",
++ "image/vnd.valve.source.texture",
++ "image/vnd.wap.wbmp",
++ "image/vnd.xiff",
++ "image/vnd.zbrush.pcx",
++ "image/webp",
++ "image/wmf",
++ "image/x-canon-cr2",
++ "image/x-canon-crw",
++ "image/x-cmu-raster",
++ "image/x-coreldraw",
++ "image/x-coreldrawpattern",
++ "image/x-coreldrawtemplate",
++ "image/x-corelphotopaint",
++ "image/x-emf",
++ "image/x-epson-erf",
++ "image/x-icon",
++ "image/x-jg",
++ "image/x-jng",
++ "image/x-ms-bmp",
++ "image/x-nikon-nef",
++ "image/x-olympus-orf",
++ "image/x-photoshop",
++ "image/x-portable-anymap",
++ "image/x-portable-bitmap",
++ "image/x-portable-graymap",
++ "image/x-portable-pixmap",
++ "image/x-rgb",
++ "image/x-wmf",
++ "image/x-xbitmap",
++ "image/x-xpixmap",
++ "image/x-xwindowdump",
++ };
++
+ private Uri fileUri;
+
+ private ReactApplicationContext reactContext;
+@@ -148,6 +262,7 @@ public class ImagePickerModuleImpl implements ActivityEventListener {
+
+ if (isPhoto) {
+ libraryIntent.setType("image/*");
++ libraryIntent.putExtra(Intent.EXTRA_MIME_TYPES, this.ALLOWED_IMAGE_MIME_TYPES);
+ } else if (isVideo) {
+ libraryIntent.setType("video/*");
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
diff --git a/src/CONST.ts b/src/CONST.ts
index ceb0617182ad..eed1b98ae551 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1163,6 +1163,7 @@ const CONST = {
},
AVATAR_SIZE: {
+ XLARGE: 'xlarge',
LARGE: 'large',
MEDIUM: 'medium',
DEFAULT: 'default',
@@ -1311,9 +1312,9 @@ const CONST = {
},
// Auth limit is 60k for the column but we store edits and other metadata along the html so let's use a lower limit to accommodate for it.
- MAX_COMMENT_LENGTH: 15000,
+ MAX_COMMENT_LENGTH: 10000,
- // Furthermore, applying markup is very resource-consuming, so let's set a slightly lower limit for that
+ // Use the same value as MAX_COMMENT_LENGTH to ensure the entire comment is parsed. Note that applying markup is very resource-consuming.
MAX_MARKUP_LENGTH: 10000,
MAX_THREAD_REPLIES_PREVIEW: 99,
@@ -2639,6 +2640,7 @@ const CONST = {
INDENTS: ' ',
PARENT_CHILD_SEPARATOR: ': ',
CATEGORY_LIST_THRESHOLD: 8,
+ TAG_LIST_THRESHOLD: 8,
DEMO_PAGES: {
SAASTR: 'SaaStrDemoSetup',
SBE: 'SbeDemoSetup',
diff --git a/src/Expensify.js b/src/Expensify.js
index fba65e42c06c..9e6ae1ff27b4 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -99,7 +99,9 @@ function Expensify(props) {
const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false);
useEffect(() => {
- if (props.isCheckingPublicRoom) return;
+ if (props.isCheckingPublicRoom) {
+ return;
+ }
setAttemptedToOpenPublicRoom(true);
}, [props.isCheckingPublicRoom]);
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index ae8c2037a8e3..05256f2b806c 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -362,7 +362,6 @@ type OnyxValues = {
[ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID]: string;
[ONYXKEYS.PREFERRED_THEME]: ValueOf;
[ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS]: boolean;
- [ONYXKEYS.RECEIPT_MODAL]: OnyxTypes.ReceiptModal;
[ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken;
[ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.OnyxUpdatesFromServer;
[ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT]: number;
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 47935e117e99..eb125a43c239 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -12,8 +12,14 @@ export default {
VALIDATE_LOGIN: 'ValidateLogin',
CONCIERGE: 'Concierge',
SETTINGS: {
+ ROOT: 'Settings_Root',
PREFERENCES: 'Settings_Preferences',
WORKSPACES: 'Settings_Workspaces',
+ SECURITY: 'Settings_Security',
+ STATUS: 'Settings_Status',
+ },
+ SAVE_THE_WORLD: {
+ ROOT: 'SaveTheWorld_Root',
},
SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop',
SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop',
diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js
index bd6492b4237b..1b4200572664 100644
--- a/src/components/AddressSearch/index.js
+++ b/src/components/AddressSearch/index.js
@@ -34,7 +34,7 @@ const propTypes = {
onBlur: PropTypes.func,
/** Error text to display */
- errorText: PropTypes.string,
+ errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]),
/** Hint text to display */
hint: PropTypes.string,
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js
new file mode 100644
index 000000000000..2c698d5c8a61
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {View, PixelRatio} from 'react-native';
+import useWindowDimensions from '../../../hooks/useWindowDimensions';
+import styles from '../../../styles/styles';
+
+const propTypes = {
+ /** Cell Container styles */
+ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
+};
+
+const defaultProps = {
+ style: [],
+};
+
+function AttachmentCarouselCellRenderer(props) {
+ const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
+ const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true);
+ const style = [props.style, styles.h100, {width: PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}];
+
+ return (
+
+ );
+}
+
+AttachmentCarouselCellRenderer.propTypes = propTypes;
+AttachmentCarouselCellRenderer.defaultProps = defaultProps;
+AttachmentCarouselCellRenderer.displayName = 'AttachmentCarouselCellRenderer';
+
+export default React.memo(AttachmentCarouselCellRenderer);
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
index b1a844e4172d..adee75cb4fa9 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage.js
@@ -41,8 +41,11 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialI
// to prevent the image transformer from flashing while still rendering
// Instead, we show the fallback image while the image transformer is loading the image
useEffect(() => {
- if (initialIsActive) setTimeout(() => setIsActive(true), 1);
- else setIsActive(false);
+ if (initialIsActive) {
+ setTimeout(() => setIsActive(true), 1);
+ } else {
+ setIsActive(false);
+ }
}, [initialIsActive]);
const [initialActivePageLoad, setInitialActivePageLoad] = useState(isActive);
@@ -51,8 +54,11 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialI
// We delay hiding the fallback image while image transformer is still rendering
useEffect(() => {
- if (isImageLoading) setShowFallback(true);
- else setTimeout(() => setShowFallback(false), 100);
+ if (isImageLoading) {
+ setShowFallback(true);
+ } else {
+ setTimeout(() => setShowFallback(false), 100);
+ }
}, [isImageLoading]);
return (
@@ -127,7 +133,9 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialI
const scaledImageHeight = imageHeight * minImageScale;
// Don't update the dimensions if they are already set
- if (dimensions?.scaledImageWidth === scaledImageWidth && dimensions?.scaledImageHeight === scaledImageHeight) return;
+ if (dimensions?.scaledImageWidth === scaledImageWidth && dimensions?.scaledImageHeight === scaledImageHeight) {
+ return;
+ }
cachedDimensions.set(source, {
...dimensions,
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
index 4475df168df2..b1c2864a05f6 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/ImageTransformer.js
@@ -306,7 +306,9 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
stopAnimation();
})
.onFinalize((evt, success) => {
- if (!success || !onTap) return;
+ if (!success || !onTap) {
+ return;
+ }
runOnJS(onTap)();
});
@@ -432,7 +434,9 @@ function ImageTransformer({imageWidth, imageHeight, imageScaleX, imageScaleY, sc
const pinchGesture = Gesture.Pinch()
.onTouchesDown((evt, state) => {
// we don't want to activate pinch gesture when we are scrolling pager
- if (!isScrolling.value) return;
+ if (!isScrolling.value) {
+ return;
+ }
state.fail();
})
diff --git a/src/components/Attachments/AttachmentCarousel/index.js b/src/components/Attachments/AttachmentCarousel/index.js
index 574cb496d02f..00b603cdd7d9 100644
--- a/src/components/Attachments/AttachmentCarousel/index.js
+++ b/src/components/Attachments/AttachmentCarousel/index.js
@@ -3,6 +3,7 @@ import {View, FlatList, PixelRatio, Keyboard} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import styles from '../../../styles/styles';
+import AttachmentCarouselCellRenderer from './AttachmentCarouselCellRenderer';
import CarouselActions from './CarouselActions';
import withWindowDimensions from '../../withWindowDimensions';
import CarouselButtons from './CarouselButtons';
@@ -12,7 +13,6 @@ import ONYXKEYS from '../../../ONYXKEYS';
import withLocalize from '../../withLocalize';
import compose from '../../../libs/compose';
import useCarouselArrows from './useCarouselArrows';
-import useWindowDimensions from '../../../hooks/useWindowDimensions';
import CarouselItem from './CarouselItem';
import Navigation from '../../../libs/Navigation/Navigation';
import BlockingView from '../../BlockingViews/BlockingView';
@@ -30,7 +30,6 @@ const viewabilityConfig = {
function AttachmentCarousel({report, reportActions, source, onNavigate, setDownloadButtonVisibility, translate}) {
const scrollRef = useRef(null);
- const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
const [containerWidth, setContainerWidth] = useState(0);
@@ -67,7 +66,9 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl
setDownloadButtonVisibility(initialPage !== -1);
// Update the parent modal's state with the source and name from the mapped attachments
- if (!_.isUndefined(attachmentsFromReport[initialPage])) onNavigate(attachmentsFromReport[initialPage]);
+ if (!_.isUndefined(attachmentsFromReport[initialPage])) {
+ onNavigate(attachmentsFromReport[initialPage]);
+ }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reportActions, compareImage]);
@@ -130,29 +131,6 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl
[containerWidth],
);
- /**
- * Defines how a container for a single attachment should be rendered
- * @param {Object} cellRendererProps
- * @returns {JSX.Element}
- */
- const renderCell = useCallback(
- (cellProps) => {
- // Use window width instead of layout width to address the issue in https://github.com/Expensify/App/issues/17760
- // considering horizontal margin and border width in centered modal
- const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true);
- const style = [cellProps.style, styles.h100, {width: PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}];
-
- return (
-
- );
- },
- [isSmallScreenWidth, windowWidth],
- );
-
/**
* Defines how a single attachment should be rendered
* @param {Object} item
@@ -224,7 +202,7 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, setDownl
windowSize={5}
maxToRenderPerBatch={3}
data={attachments}
- CellRendererComponent={renderCell}
+ CellRendererComponent={AttachmentCarouselCellRenderer}
renderItem={renderItem}
getItemLayout={getItemLayout}
keyExtractor={(item) => item.source}
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.js b/src/components/Attachments/AttachmentCarousel/index.native.js
index a7a2f35a2ccc..bd12020341be 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.js
+++ b/src/components/Attachments/AttachmentCarousel/index.native.js
@@ -56,7 +56,9 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose,
setDownloadButtonVisibility(initialPage !== -1);
// Update the parent modal's state with the source and name from the mapped attachments
- if (!_.isUndefined(attachmentsFromReport[initialPage])) onNavigate(attachmentsFromReport[initialPage]);
+ if (!_.isUndefined(attachmentsFromReport[initialPage])) {
+ onNavigate(attachmentsFromReport[initialPage]);
+ }
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reportActions, compareImage]);
@@ -148,7 +150,9 @@ function AttachmentCarousel({report, reportActions, source, onNavigate, onClose,
onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
onPinchGestureChange={(newIsPinchGestureRunning) => {
setIsPinchGestureRunning(newIsPinchGestureRunning);
- if (!newIsPinchGestureRunning && !shouldShowArrows) setShouldShowArrows(true);
+ if (!newIsPinchGestureRunning && !shouldShowArrows) {
+ setShouldShowArrows(true);
+ }
}}
onSwipeDown={onClose}
containerWidth={containerDimensions.width}
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
index 0767b2b68985..fdf151c4d5d0 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.native.js
@@ -25,7 +25,9 @@ function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, isUsedInCarouse
attachmentCarouselPagerContext.onPinchGestureChange(!shouldPagerScroll);
- if (attachmentCarouselPagerContext.shouldPagerScroll.value === shouldPagerScroll) return;
+ if (attachmentCarouselPagerContext.shouldPagerScroll.value === shouldPagerScroll) {
+ return;
+ }
attachmentCarouselPagerContext.shouldPagerScroll.value = shouldPagerScroll;
}
diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js
index da7b77ca193e..90f72f183815 100644
--- a/src/components/CategoryPicker/index.js
+++ b/src/components/CategoryPicker/index.js
@@ -43,7 +43,7 @@ function CategoryPicker({selectedCategory, policyCategories, policyRecentlyUsedC
}, [policyCategories, selectedOptions, isCategoriesCountBelowThreshold]);
const sections = useMemo(
- () => OptionsListUtils.getNewChatOptions({}, {}, [], searchValue, selectedOptions, [], false, false, true, policyCategories, policyRecentlyUsedCategories, false).categoryOptions,
+ () => OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, true, policyCategories, policyRecentlyUsedCategories, false).categoryOptions,
[policyCategories, policyRecentlyUsedCategories, searchValue, selectedOptions],
);
diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js
index d0805cbcc7c3..1132efa9e50e 100644
--- a/src/components/Composer/index.android.js
+++ b/src/components/Composer/index.android.js
@@ -97,7 +97,9 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC
* @return {Number}
*/
const maxNumberOfLines = useMemo(() => {
- if (isComposerFullSize) return 1000000;
+ if (isComposerFullSize) {
+ return 1000000;
+ }
return maxLines;
}, [isComposerFullSize, maxLines]);
diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js
index c0a3859e6d01..0b2c93f6639e 100644
--- a/src/components/Composer/index.ios.js
+++ b/src/components/Composer/index.ios.js
@@ -97,7 +97,9 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC
* @return {Number}
*/
const maxNumberOfLines = useMemo(() => {
- if (isComposerFullSize) return undefined;
+ if (isComposerFullSize) {
+ return;
+ }
return maxLines;
}, [isComposerFullSize, maxLines]);
diff --git a/src/components/CurrentUserPersonalDetailsSkeletonView/index.js b/src/components/CurrentUserPersonalDetailsSkeletonView/index.js
index 6e6c46e971c0..cc305a628820 100644
--- a/src/components/CurrentUserPersonalDetailsSkeletonView/index.js
+++ b/src/components/CurrentUserPersonalDetailsSkeletonView/index.js
@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
+import _ from 'underscore';
import SkeletonViewContentLoader from 'react-content-loader/native';
import {Circle, Rect} from 'react-native-svg';
import {View} from 'react-native';
@@ -12,14 +13,26 @@ import styles from '../../styles/styles';
const propTypes = {
/** Whether to animate the skeleton view */
shouldAnimate: PropTypes.bool,
+
+ /** The size of the avatar */
+ avatarSize: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)),
+
+ /** Background color of the skeleton view */
+ backgroundColor: PropTypes.string,
+
+ /** Foreground color of the skeleton view */
+ foregroundColor: PropTypes.string,
};
const defaultProps = {
shouldAnimate: true,
+ avatarSize: CONST.AVATAR_SIZE.LARGE,
+ backgroundColor: themeColors.highlightBG,
+ foregroundColor: themeColors.border,
};
function CurrentUserPersonalDetailsSkeletonView(props) {
- const avatarPlaceholderSize = StyleUtils.getAvatarSize(CONST.AVATAR_SIZE.LARGE);
+ const avatarPlaceholderSize = StyleUtils.getAvatarSize(props.avatarSize);
const avatarPlaceholderRadius = avatarPlaceholderSize / 2;
const spaceBetweenAvatarAndHeadline = styles.mb3.marginBottom + styles.mt1.marginTop + (variables.lineHeightXXLarge - variables.fontSizeXLarge) / 2;
const headlineSize = variables.fontSizeXLarge;
@@ -29,8 +42,8 @@ function CurrentUserPersonalDetailsSkeletonView(props) {
Transaction.addStop(iou.transactionID)}
+ onPress={() => {
+ const newIndex = _.size(lodashGet(transaction, 'comment.waypoints', {}));
+ Navigation.navigate(ROUTES.getMoneyRequestWaypointRoute('request', newIndex));
+ }}
text={translate('distance.addStop')}
isDisabled={numberOfWaypoints === MAX_WAYPOINTS}
innerStyles={[styles.ph10]}
@@ -294,7 +297,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken,
success
style={[styles.w100, styles.mb4, styles.ph4, styles.flexShrink0]}
onPress={navigateToNextPage}
- isDisabled={_.size(validatedWaypoints) < 2 || hasRouteError || isOffline}
+ isDisabled={_.size(validatedWaypoints) < 2 || hasRouteError}
text={translate('common.next')}
/>
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index d3268ebc54b0..27fd199a3895 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -104,7 +104,9 @@ class EmojiPickerMenu extends Component {
}
componentDidUpdate(prevProps) {
- if (prevProps.frequentlyUsedEmojis === this.props.frequentlyUsedEmojis) return;
+ if (prevProps.frequentlyUsedEmojis === this.props.frequentlyUsedEmojis) {
+ return;
+ }
const {filteredEmojis, headerEmojis, headerRowIndices} = this.getEmojisAndHeaderRowIndices();
this.emojis = filteredEmojis;
diff --git a/src/components/ExceededCommentLength.js b/src/components/ExceededCommentLength.js
index 2aa50779e10f..7c9ec4d2db25 100644
--- a/src/components/ExceededCommentLength.js
+++ b/src/components/ExceededCommentLength.js
@@ -4,6 +4,7 @@ import {debounce} from 'lodash';
import {withOnyx} from 'react-native-onyx';
import CONST from '../CONST';
import * as ReportUtils from '../libs/ReportUtils';
+import useLocalize from '../hooks/useLocalize';
import Text from './Text';
import styles from '../styles/styles';
import ONYXKEYS from '../ONYXKEYS';
@@ -25,6 +26,7 @@ const defaultProps = {
};
function ExceededCommentLength(props) {
+ const {numberFormat, translate} = useLocalize();
const [commentLength, setCommentLength] = useState(0);
const updateCommentLength = useMemo(
() =>
@@ -44,7 +46,14 @@ function ExceededCommentLength(props) {
return null;
}
- return {`${commentLength}/${CONST.MAX_COMMENT_LENGTH}`};
+ return (
+
+ {translate('composer.commentExceededMaxLength', {formattedMaxLength: numberFormat(CONST.MAX_COMMENT_LENGTH)})}
+
+ );
}
ExceededCommentLength.propTypes = propTypes;
diff --git a/src/components/StaticHeaderPageLayout.js b/src/components/HeaderPageLayout.js
similarity index 53%
rename from src/components/StaticHeaderPageLayout.js
rename to src/components/HeaderPageLayout.js
index f97e42329942..bec1e52b1cad 100644
--- a/src/components/StaticHeaderPageLayout.js
+++ b/src/components/HeaderPageLayout.js
@@ -10,6 +10,8 @@ import themeColors from '../styles/themes/default';
import * as StyleUtils from '../styles/StyleUtils';
import useWindowDimensions from '../hooks/useWindowDimensions';
import FixedFooter from './FixedFooter';
+import useNetwork from '../hooks/useNetwork';
+import * as Browser from '../libs/Browser';
const propTypes = {
...headerWithBackButtonPropTypes,
@@ -22,16 +24,26 @@ const propTypes = {
/** A fixed footer to display at the bottom of the page. */
footer: PropTypes.node,
+
+ /** The image to display in the upper half of the screen. */
+ header: PropTypes.node,
+
+ /** Style to apply to the header image container */
+ // eslint-disable-next-line react/forbid-prop-types
+ headerContainerStyles: PropTypes.arrayOf(PropTypes.object),
};
const defaultProps = {
backgroundColor: themeColors.appBG,
+ header: null,
+ headerContainerStyles: [],
footer: null,
};
-function StaticHeaderPageLayout({backgroundColor, children, image: Image, footer, imageContainerStyle, style, ...propsToPassToHeader}) {
- const {windowHeight} = useWindowDimensions();
-
+function HeaderPageLayout({backgroundColor, children, footer, headerContainerStyles, style, headerContent, ...propsToPassToHeader}) {
+ const {windowHeight, isSmallScreenWidth} = useWindowDimensions();
+ const {isOffline} = useNetwork();
+ const appBGColor = StyleUtils.getBackgroundColorStyle(themeColors.appBG);
const {titleColor, iconFill} = useMemo(() => {
const isColorfulBackground = backgroundColor !== themeColors.appBG;
return {
@@ -45,7 +57,7 @@ function StaticHeaderPageLayout({backgroundColor, children, image: Image, footer
style={[StyleUtils.getBackgroundColorStyle(backgroundColor)]}
shouldEnablePickerAvoiding={false}
includeSafeAreaPaddingBottom={false}
- offlineIndicatorStyle={[StyleUtils.getBackgroundColorStyle(themeColors.appBG)]}
+ offlineIndicatorStyle={[appBGColor]}
>
{({safeAreaPaddingBottomStyle}) => (
<>
@@ -55,27 +67,24 @@ function StaticHeaderPageLayout({backgroundColor, children, image: Image, footer
titleColor={titleColor}
iconFill={iconFill}
/>
-
+
+ {/** Safari on ios/mac has a bug where overscrolling the page scrollview shows green background color. This is a workaround to fix that. https://github.com/Expensify/App/issues/23422 */}
+ {Browser.isSafari() && (
+
+
+
+
+ )}
-
-
-
+ {!Browser.isSafari() && }
+
+ {headerContent}
- {children}
+ {children}
{!_.isNull(footer) && {footer}}
@@ -85,8 +94,8 @@ function StaticHeaderPageLayout({backgroundColor, children, image: Image, footer
);
}
-StaticHeaderPageLayout.propTypes = propTypes;
-StaticHeaderPageLayout.defaultProps = defaultProps;
-StaticHeaderPageLayout.displayName = 'StaticHeaderPageLayout';
+HeaderPageLayout.propTypes = propTypes;
+HeaderPageLayout.defaultProps = defaultProps;
+HeaderPageLayout.displayName = 'HeaderPageLayout';
-export default StaticHeaderPageLayout;
+export default HeaderPageLayout;
diff --git a/src/components/Hoverable/index.js b/src/components/Hoverable/index.js
index 5da41f1388fb..7dd918f15cf4 100644
--- a/src/components/Hoverable/index.js
+++ b/src/components/Hoverable/index.js
@@ -91,7 +91,9 @@ class Hoverable extends Component {
/**
* If the isScrollingRef is true, then the user is scrolling and we should not update the hover state.
*/
- if (this.isScrollingRef && this.props.shouldHandleScroll && !this.state.isHovered) return;
+ if (this.isScrollingRef && this.props.shouldHandleScroll && !this.state.isHovered) {
+ return;
+ }
if (isHovered !== this.state.isHovered) {
this.setState({isHovered}, isHovered ? this.props.onHoverIn : this.props.onHoverOut);
diff --git a/src/components/IllustratedHeaderPageLayout.js b/src/components/IllustratedHeaderPageLayout.js
index 92a9c8b8552b..ac916117094b 100644
--- a/src/components/IllustratedHeaderPageLayout.js
+++ b/src/components/IllustratedHeaderPageLayout.js
@@ -1,18 +1,10 @@
-import _ from 'underscore';
import React from 'react';
import PropTypes from 'prop-types';
-import {ScrollView, View} from 'react-native';
import Lottie from 'lottie-react-native';
import headerWithBackButtonPropTypes from './HeaderWithBackButton/headerWithBackButtonPropTypes';
-import HeaderWithBackButton from './HeaderWithBackButton';
-import ScreenWrapper from './ScreenWrapper';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
-import * as StyleUtils from '../styles/StyleUtils';
-import useWindowDimensions from '../hooks/useWindowDimensions';
-import FixedFooter from './FixedFooter';
-import useNetwork from '../hooks/useNetwork';
-import * as Browser from '../libs/Browser';
+import HeaderPageLayout from './HeaderPageLayout';
const propTypes = {
...headerWithBackButtonPropTypes,
@@ -40,54 +32,28 @@ const defaultProps = {
};
function IllustratedHeaderPageLayout({backgroundColor, children, illustration, footer, overlayContent, ...propsToPassToHeader}) {
- const {isOffline} = useNetwork();
- const {isSmallScreenWidth, windowHeight} = useWindowDimensions();
- const appBGColor = StyleUtils.getBackgroundColorStyle(themeColors.appBG);
-
return (
-
- {({safeAreaPaddingBottomStyle}) => (
+
-
-
- {/* Safari on ios/mac has a bug where overscrolling the page scrollview shows green the background color. This is a workaround to fix that. https://github.com/Expensify/App/issues/23422 */}
- {Browser.isSafari() && (
-
-
-
-
- )}
-
- {!Browser.isSafari() && }
-
-
- {overlayContent && overlayContent()}
-
- {children}
-
- {!_.isNull(footer) && {footer}}
-
+ {overlayContent && overlayContent()}
>
- )}
-
+ }
+ headerContainerStyles={[styles.justifyContentCenter, styles.w100]}
+ footer={footer}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...propsToPassToHeader}
+ >
+ {children}
+
);
}
diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.js
index c92bd7738253..e0dce180043b 100644
--- a/src/components/ImageView/index.js
+++ b/src/components/ImageView/index.js
@@ -87,7 +87,9 @@ function ImageView({isAuthTokenRequired, url, fileName}) {
};
const imageLoadingStart = () => {
- if (!isLoading) return;
+ if (!isLoading) {
+ return;
+ }
setIsLoading(true);
setZoomScale(0);
setIsZoomed(false);
@@ -141,11 +143,16 @@ function ImageView({isAuthTokenRequired, url, fileName}) {
*/
const onContainerPress = (e) => {
if (!isZoomed && !isDragging) {
- const {offsetX, offsetY} = e.nativeEvent;
- // Dividing clicked positions by the zoom scale to get coordinates
- // so that once we zoom we will scroll to the clicked location.
- const delta = getScrollOffset(offsetX / zoomScale, offsetY / zoomScale);
- setZoomDelta(delta);
+ if (e.nativeEvent) {
+ const {offsetX, offsetY} = e.nativeEvent;
+
+ // Dividing clicked positions by the zoom scale to get coordinates
+ // so that once we zoom we will scroll to the clicked location.
+ const delta = getScrollOffset(offsetX / zoomScale, offsetY / zoomScale);
+ setZoomDelta(delta);
+ } else {
+ setZoomDelta({offsetX: 0, offsetY: 0});
+ }
}
if (isZoomed && isDragging && isMouseDown) {
@@ -225,14 +232,14 @@ function ImageView({isAuthTokenRequired, url, fileName}) {
source={{uri: url}}
isAuthTokenRequired={isAuthTokenRequired}
// Hide image until finished loading to prevent showing preview with wrong dimensions.
- style={isLoading ? undefined : [styles.w100, styles.h100]}
+ style={isLoading || zoomScale === 0 ? undefined : [styles.w100, styles.h100]}
// When Image dimensions are lower than the container boundary(zoomscale <= 1), use `contain` to render the image with natural dimensions.
// Both `center` and `contain` keeps the image centered on both x and y axis.
resizeMode={zoomScale > 1 ? Image.resizeMode.center : Image.resizeMode.contain}
onLoadStart={imageLoadingStart}
onLoad={imageLoad}
/>
- {isLoading && }
+ {(isLoading || zoomScale === 0) && }
);
}
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 531bbdad3977..13471407914f 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -207,7 +207,7 @@ function MoneyRequestConfirmationList(props) {
const tagListName = lodashGet(props.policyTags, [tagListKey, 'name'], '');
const canUseTags = Permissions.canUseTags(props.betas);
// A flag for showing the tags field
- const shouldShowTags = isPolicyExpenseChat && canUseTags && !_.isEmpty(tagList);
+ const shouldShowTags = isPolicyExpenseChat && canUseTags && _.any(tagList, (tag) => tag.enabled);
// A flag for showing the billable field
const shouldShowBillable = canUseTags && !lodashGet(props.policy, 'disabledFields.defaultBillable', true);
diff --git a/src/components/Pressable/PressableWithFeedback.js b/src/components/Pressable/PressableWithFeedback.js
index 7eb0ee7286c9..40be99823ceb 100644
--- a/src/components/Pressable/PressableWithFeedback.js
+++ b/src/components/Pressable/PressableWithFeedback.js
@@ -58,19 +58,27 @@ const PressableWithFeedback = forwardRef((props, ref) => {
isExecuting={isExecuting}
onHoverIn={() => {
setIsHovered(true);
- if (props.onHoverIn) props.onHoverIn();
+ if (props.onHoverIn) {
+ props.onHoverIn();
+ }
}}
onHoverOut={() => {
setIsHovered(false);
- if (props.onHoverOut) props.onHoverOut();
+ if (props.onHoverOut) {
+ props.onHoverOut();
+ }
}}
onPressIn={() => {
setIsPressed(true);
- if (props.onPressIn) props.onPressIn();
+ if (props.onPressIn) {
+ props.onPressIn();
+ }
}}
onPressOut={() => {
setIsPressed(false);
- if (props.onPressOut) props.onPressOut();
+ if (props.onPressOut) {
+ props.onPressOut();
+ }
}}
onPress={(e) => {
singleExecution(() => props.onPress(e))();
diff --git a/src/components/QRShare/QRShareWithDownload/index.js b/src/components/QRShare/QRShareWithDownload/index.js
index 310122b96d40..665115823357 100644
--- a/src/components/QRShare/QRShareWithDownload/index.js
+++ b/src/components/QRShare/QRShareWithDownload/index.js
@@ -17,7 +17,9 @@ class QRShareWithDownload extends Component {
return new Promise((resolve, reject) => {
// eslint-disable-next-line es/no-optional-chaining
const svg = this.qrShareRef.current?.getSvg();
- if (svg == null) return reject();
+ if (svg == null) {
+ return reject();
+ }
svg.toDataURL((dataURL) => resolve(fileDownload(dataURL, getQrCodeFileName(this.props.title))));
});
diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js
index 3a0741cf9bd2..2afc6240f85d 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview.js
+++ b/src/components/ReportActionItem/MoneyRequestPreview.js
@@ -170,7 +170,7 @@ function MoneyRequestPreview(props) {
!_.isEmpty(requestMerchant) && !props.isBillSplit && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT;
const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant;
- const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction.receipt.source, props.transaction.filename || props.transaction.receiptFilename || '')] : [];
+ const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction.receipt.source, props.transaction.filename || '')] : [];
const getSettledMessage = () => {
switch (lodashGet(props.action, 'originalMessage.paymentType', '')) {
diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js
index c14e40010b54..1350c62bda88 100644
--- a/src/components/ReportActionItem/ReportPreview.js
+++ b/src/components/ReportActionItem/ReportPreview.js
@@ -120,9 +120,7 @@ function ReportPreview(props) {
const isScanning = hasReceipts && ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action);
const hasErrors = hasReceipts && ReportUtils.hasMissingSmartscanFields(props.iouReportID);
const lastThreeTransactionsWithReceipts = ReportUtils.getReportPreviewDisplayTransactions(props.action);
- const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, ({receipt, filename, receiptFilename}) =>
- ReceiptUtils.getThumbnailAndImageURIs(receipt.source, filename || receiptFilename || ''),
- );
+ const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, ({receipt, filename}) => ReceiptUtils.getThumbnailAndImageURIs(receipt.source, filename || ''));
const hasOnlyOneReceiptRequest = numberOfRequests === 1 && hasReceipts;
const previewSubtitle = hasOnlyOneReceiptRequest
diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js
index f2a1758a050b..ae77a18b980f 100644
--- a/src/components/ReportActionItem/TaskView.js
+++ b/src/components/ReportActionItem/TaskView.js
@@ -51,7 +51,8 @@ function TaskView(props) {
const isOpen = ReportUtils.isOpenTaskReport(props.report);
const isCanceled = ReportUtils.isCanceledTaskReport(props.report);
const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID);
- const disableState = !canModifyTask || !isOpen;
+ const disableState = !canModifyTask || isCanceled;
+ const isDisableInteractive = !canModifyTask || !isOpen;
return (
(
{
+ if (isDisableInteractive) {
+ return;
+ }
if (e && e.type === 'click') {
e.currentTarget.blur();
}
Navigation.navigate(ROUTES.getTaskReportTitleRoute(props.report.reportID));
})}
- style={({pressed}) => [styles.ph5, styles.pv2, StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, false, disableState), true)]}
+ style={({pressed}) => [
+ styles.ph5,
+ styles.pv2,
+ StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, false, disableState, !isDisableInteractive), true),
+ isDisableInteractive && !disableState && styles.cursorDefault,
+ ]}
ref={props.forwardedRef}
disabled={disableState}
accessibilityLabel={taskTitle || props.translate('task.task')}
@@ -129,6 +138,7 @@ function TaskView(props) {
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
shouldGreyOutWhenDisabled={false}
numberOfLinesTitle={0}
+ interactive={!isDisableInteractive}
/>
{props.report.managerID ? (
@@ -146,6 +156,7 @@ function TaskView(props) {
wrapperStyle={[styles.pv2]}
isSmallAvatarSubscriptMenu
shouldGreyOutWhenDisabled={false}
+ interactive={!isDisableInteractive}
/>
) : (
@@ -156,6 +167,7 @@ function TaskView(props) {
disabled={disableState}
wrapperStyle={[styles.pv2]}
shouldGreyOutWhenDisabled={false}
+ interactive={!isDisableInteractive}
/>
)}
diff --git a/src/components/SignInButtons/AppleSignIn/index.android.js b/src/components/SignInButtons/AppleSignIn/index.android.js
index 83a99683b178..48d2bf3cc861 100644
--- a/src/components/SignInButtons/AppleSignIn/index.android.js
+++ b/src/components/SignInButtons/AppleSignIn/index.android.js
@@ -39,7 +39,9 @@ function AppleSignIn() {
appleSignInRequest()
.then((token) => Session.beginAppleSignIn(token))
.catch((e) => {
- if (e.message === appleAuthAndroid.Error.SIGNIN_CANCELLED) return null;
+ if (e.message === appleAuthAndroid.Error.SIGNIN_CANCELLED) {
+ return null;
+ }
Log.alert('[Apple Sign In] Apple authentication failed', e);
});
};
diff --git a/src/components/SignInButtons/AppleSignIn/index.ios.js b/src/components/SignInButtons/AppleSignIn/index.ios.js
index 681eebb298c5..0c9a8c9e8211 100644
--- a/src/components/SignInButtons/AppleSignIn/index.ios.js
+++ b/src/components/SignInButtons/AppleSignIn/index.ios.js
@@ -37,7 +37,9 @@ function AppleSignIn() {
appleSignInRequest()
.then((token) => Session.beginAppleSignIn(token))
.catch((e) => {
- if (e.code === appleAuth.Error.CANCELED) return null;
+ if (e.code === appleAuth.Error.CANCELED) {
+ return null;
+ }
Log.alert('[Apple Sign In] Apple authentication failed', e);
});
};
diff --git a/src/components/SignInButtons/AppleSignIn/index.website.js b/src/components/SignInButtons/AppleSignIn/index.website.js
index 41c8f2afd4d5..7046de5068b1 100644
--- a/src/components/SignInButtons/AppleSignIn/index.website.js
+++ b/src/components/SignInButtons/AppleSignIn/index.website.js
@@ -55,7 +55,9 @@ const successListener = (event) => {
};
const failureListener = (event) => {
- if (!event.detail || event.detail.error === 'popup_closed_by_user') return null;
+ if (!event.detail || event.detail.error === 'popup_closed_by_user') {
+ return null;
+ }
Log.warn(`Apple sign-in failed: ${event.detail}`);
};
@@ -126,7 +128,9 @@ const SingletonAppleSignInButtonWithFocus = withNavigationFocus(SingletonAppleSi
function AppleSignIn({isDesktopFlow}) {
const [scriptLoaded, setScriptLoaded] = useState(false);
useEffect(() => {
- if (window.appleAuthScriptLoaded) return;
+ if (window.appleAuthScriptLoaded) {
+ return;
+ }
const localeCode = getUserLanguage();
const script = document.createElement('script');
diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.js
index 25021bd817d7..c46ca1b57b22 100644
--- a/src/components/TagPicker/index.js
+++ b/src/components/TagPicker/index.js
@@ -1,62 +1,58 @@
-import React, {useMemo} from 'react';
+import React, {useMemo, useState} from 'react';
import _ from 'underscore';
import lodashGet from 'lodash/get';
import {withOnyx} from 'react-native-onyx';
+import CONST from '../../CONST';
import ONYXKEYS from '../../ONYXKEYS';
import styles from '../../styles/styles';
-import Navigation from '../../libs/Navigation/Navigation';
-import ROUTES from '../../ROUTES';
import useLocalize from '../../hooks/useLocalize';
import * as OptionsListUtils from '../../libs/OptionsListUtils';
import OptionsSelector from '../OptionsSelector';
import {propTypes, defaultProps} from './tagPickerPropTypes';
-function TagPicker({policyTags, reportID, tag, iouType, iou}) {
+function TagPicker({selectedTag, tag, policyTags, policyRecentlyUsedTags, onSubmit}) {
const {translate} = useLocalize();
+ const [searchValue, setSearchValue] = useState('');
+
+ const policyRecentlyUsedTagsList = lodashGet(policyRecentlyUsedTags, tag, []);
+ const policyTagList = lodashGet(policyTags, [tag, 'tags'], {});
+ const policyTagsCount = _.size(_.filter(policyTagList, (policyTag) => policyTag.enabled));
+ const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD;
+
+ const shouldShowTextInput = !isTagsCountBelowThreshold;
const selectedOptions = useMemo(() => {
- if (!iou.tag) {
+ if (!selectedTag) {
return [];
}
return [
{
- name: iou.tag,
+ name: selectedTag,
enabled: true,
+ accountID: null,
},
];
- }, [iou.tag]);
-
- // Only shows one section, which will be the default behavior if there are
- // less than 8 policy tags
- // TODO: support sections with search
- const sections = useMemo(() => {
- const tagList = _.chain(lodashGet(policyTags, [tag, 'tags'], {}))
- .values()
- .map((t) => ({
- text: t.name,
- keyForList: t.name,
- tooltipText: t.name,
- }))
- .value();
+ }, [selectedTag]);
- return [
- {
- data: tagList,
- },
- ];
- }, [policyTags, tag]);
+ const initialFocusedIndex = useMemo(() => {
+ if (isTagsCountBelowThreshold && selectedOptions.length > 0) {
+ return _.chain(policyTagList)
+ .values()
+ .findIndex((policyTag) => policyTag.name === selectedOptions[0].name, true)
+ .value();
+ }
- const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, '');
+ return 0;
+ }, [policyTagList, selectedOptions, isTagsCountBelowThreshold]);
- const navigateBack = () => {
- Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID));
- };
+ const sections = useMemo(
+ () =>
+ OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, policyTagList, policyRecentlyUsedTagsList, false).tagOptions,
+ [searchValue, selectedOptions, policyTagList, policyRecentlyUsedTagsList],
+ );
- const updateTag = () => {
- // TODO: add logic to save the selected tag
- navigateBack();
- };
+ const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, '');
return (
);
}
@@ -84,7 +84,4 @@ export default withOnyx({
policyRecentlyUsedTags: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`,
},
- iou: {
- key: ONYXKEYS.IOU,
- },
})(TagPicker);
diff --git a/src/components/TagPicker/tagPickerPropTypes.js b/src/components/TagPicker/tagPickerPropTypes.js
index ad57a0409f15..a5d94605a76a 100644
--- a/src/components/TagPicker/tagPickerPropTypes.js
+++ b/src/components/TagPicker/tagPickerPropTypes.js
@@ -1,22 +1,18 @@
import PropTypes from 'prop-types';
import tagPropTypes from '../tagPropTypes';
-import {iouPropTypes, iouDefaultProps} from '../../pages/iou/propTypes';
const propTypes = {
- /** The report ID of the IOU */
- reportID: PropTypes.string.isRequired,
-
/** The policyID we are getting tags for */
policyID: PropTypes.string.isRequired,
+ /** The selected tag of the money request */
+ selectedTag: PropTypes.string.isRequired,
+
/** The name of tag list we are getting tags for */
tag: PropTypes.string.isRequired,
- /** The type of IOU report, i.e. bill, request, send */
- iouType: PropTypes.string.isRequired,
-
/** Callback to submit the selected tag */
- onSubmit: PropTypes.func,
+ onSubmit: PropTypes.func.isRequired,
/* Onyx Props */
/** Collection of tags attached to a policy */
@@ -29,15 +25,11 @@ const propTypes = {
/** List of recently used tags */
policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)),
-
- /** Holds data related to Money Request view state, rather than the underlying Money Request data. */
- iou: iouPropTypes,
};
const defaultProps = {
policyTags: {},
policyRecentlyUsedTags: {},
- iou: iouDefaultProps,
};
export {propTypes, defaultProps};
diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.js
index 48a92f081200..6cefe04e71a1 100644
--- a/src/components/TextInput/index.js
+++ b/src/components/TextInput/index.js
@@ -30,7 +30,9 @@ function TextInput(props) {
});
return () => {
- if (!removeVisibilityListenerRef.current) return;
+ if (!removeVisibilityListenerRef.current) {
+ return;
+ }
removeVisibilityListenerRef.current();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/src/components/withWindowDimensions/index.js b/src/components/withWindowDimensions/index.js
index a3836fa99e6b..37d5c94688a2 100644
--- a/src/components/withWindowDimensions/index.js
+++ b/src/components/withWindowDimensions/index.js
@@ -1,5 +1,6 @@
import React, {forwardRef, createContext, useState, useEffect} from 'react';
import PropTypes from 'prop-types';
+import lodashDebounce from 'lodash/debounce';
import {Dimensions} from 'react-native';
import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
import getComponentDisplayName from '../../libs/getComponentDisplayName';
@@ -44,14 +45,15 @@ function WindowDimensionsProvider(props) {
useEffect(() => {
const onDimensionChange = (newDimensions) => {
const {window} = newDimensions;
-
setWindowDimension({
windowHeight: window.height,
windowWidth: window.width,
});
};
- const dimensionsEventListener = Dimensions.addEventListener('change', onDimensionChange);
+ const onDimensionChangeDebounce = lodashDebounce(onDimensionChange, 300);
+
+ const dimensionsEventListener = Dimensions.addEventListener('change', onDimensionChangeDebounce);
return () => {
if (!dimensionsEventListener) {
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 210d82b28a7d..f7c028d2a106 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -69,6 +69,7 @@ import type {
SetTheRequestParams,
UpdatedTheRequestParams,
RemovedTheRequestParams,
+ FormattedMaxLengthParams,
RequestedAmountMessageParams,
TagSelectionParams,
TranslationBase,
@@ -282,6 +283,7 @@ export default {
composer: {
noExtensionFoundForMimeType: 'No extension found for mime type',
problemGettingImageYouPasted: 'There was a problem getting the image you pasted',
+ commentExceededMaxLength: ({formattedMaxLength}: FormattedMaxLengthParams) => `The maximum comment length is ${formattedMaxLength} characters.`,
},
baseUpdateAppModal: {
updateApp: 'Update app',
@@ -541,6 +543,7 @@ export default {
threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`,
tagSelection: ({tagName}: TagSelectionParams) => `Select a ${tagName} to add additional organization to your money`,
error: {
+ invalidAmount: 'Please enter a valid amount before continuing.',
invalidSplit: 'Split amounts do not equal total amount',
other: 'Unexpected error, please try again later',
genericCreateFailureMessage: 'Unexpected error requesting money, please try again later',
@@ -729,6 +732,7 @@ export default {
keepCodesSafe: 'Keep these recovery codes safe!',
codesLoseAccess:
'If you lose access to your authenticator app and don’t have these codes, you will lose access to your account. \n\nNote: Setting up two-factor authentication will log you out of all other active sessions.',
+ errorStepCodes: 'Please copy or download codes before continuing.',
stepVerify: 'Verify',
scanCode: 'Scan the QR code using your',
authenticatorApp: 'authenticator app',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 0048cfbb9e23..a68f33a33730 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -69,6 +69,7 @@ import type {
SetTheRequestParams,
UpdatedTheRequestParams,
RemovedTheRequestParams,
+ FormattedMaxLengthParams,
RequestedAmountMessageParams,
TagSelectionParams,
EnglishTranslation,
@@ -272,6 +273,7 @@ export default {
composer: {
noExtensionFoundForMimeType: 'No se encontró una extension para este tipo de contenido',
problemGettingImageYouPasted: 'Ha ocurrido un problema al obtener la imagen que has pegado',
+ commentExceededMaxLength: ({formattedMaxLength}: FormattedMaxLengthParams) => `El comentario debe tener máximo ${formattedMaxLength} caracteres.`,
},
baseUpdateAppModal: {
updateApp: 'Actualizar app',
@@ -534,6 +536,7 @@ export default {
threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`,
tagSelection: ({tagName}: TagSelectionParams) => `Seleccione una ${tagName} para organizar mejor tu dinero`,
error: {
+ invalidAmount: 'Por favor ingresa un monto válido antes de continuar.',
invalidSplit: 'La suma de las partes no equivale al monto total',
other: 'Error inesperado, por favor inténtalo más tarde',
genericCreateFailureMessage: 'Error inesperado solicitando dinero, Por favor, inténtalo más tarde',
@@ -724,6 +727,7 @@ export default {
keepCodesSafe: '¡Guarda los códigos de recuperación en un lugar seguro!',
codesLoseAccess:
'Si pierdes el acceso a tu aplicación de autenticación y no tienes estos códigos, perderás el acceso a tu cuenta. \n\nNota: Configurar la autenticación de dos factores cerrará la sesión de todas las demás sesiones activas.',
+ errorStepCodes: 'Copia o descarga los códigos antes de continuar.',
stepVerify: 'Verificar',
scanCode: 'Escanea el código QR usando tu',
authenticatorApp: 'aplicación de autenticación',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index 9af00ceef8de..70bf2e4cae3d 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -190,6 +190,8 @@ type RemovedTheRequestParams = {valueName: string; oldValueToDisplay: string};
type UpdatedTheRequestParams = {valueName: string; newValueToDisplay: string; oldValueToDisplay: string};
+type FormattedMaxLengthParams = {formattedMaxLength: string};
+
type TagSelectionParams = {tagName: string};
/* Translation Object types */
@@ -303,5 +305,6 @@ export type {
SetTheRequestParams,
UpdatedTheRequestParams,
RemovedTheRequestParams,
+ FormattedMaxLengthParams,
TagSelectionParams,
};
diff --git a/src/libs/BootSplash/index.js b/src/libs/BootSplash/index.js
index ff7ab5562b1f..c169f380a8eb 100644
--- a/src/libs/BootSplash/index.js
+++ b/src/libs/BootSplash/index.js
@@ -9,10 +9,14 @@ function hide() {
return document.fonts.ready.then(() => {
const splash = document.getElementById('splash');
- if (splash) splash.style.opacity = 0;
+ if (splash) {
+ splash.style.opacity = 0;
+ }
return resolveAfter(250).then(() => {
- if (!splash || !splash.parentNode) return;
+ if (!splash || !splash.parentNode) {
+ return;
+ }
splash.parentNode.removeChild(splash);
});
});
diff --git a/src/libs/ComposerUtils/getDraftComment.js b/src/libs/ComposerUtils/getDraftComment.js
index ddcb966bb2a7..854df1ac65ee 100644
--- a/src/libs/ComposerUtils/getDraftComment.js
+++ b/src/libs/ComposerUtils/getDraftComment.js
@@ -5,7 +5,9 @@ const draftCommentMap = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT,
callback: (value, key) => {
- if (!key) return;
+ if (!key) {
+ return;
+ }
const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, '');
draftCommentMap[reportID] = value;
diff --git a/src/libs/CurrencyUtils.js b/src/libs/CurrencyUtils.js
index 6cbb0db7661b..5cf0b22ef337 100644
--- a/src/libs/CurrencyUtils.js
+++ b/src/libs/CurrencyUtils.js
@@ -128,4 +128,25 @@ function convertToDisplayString(amountInCents, currency = CONST.CURRENCY.USD) {
});
}
-export {getCurrencyDecimals, getCurrencyUnit, getLocalizedCurrencySymbol, getCurrencySymbol, isCurrencySymbolLTR, convertToBackendAmount, convertToFrontendAmount, convertToDisplayString};
+/**
+ * Checks if passed currency code is a valid currency based on currency list
+ *
+ * @param {String} currencyCode
+ * @returns {Boolean}
+ */
+function isValidCurrencyCode(currencyCode) {
+ const currency = lodashGet(currencyList, currencyCode);
+ return Boolean(currency);
+}
+
+export {
+ getCurrencyDecimals,
+ getCurrencyUnit,
+ getLocalizedCurrencySymbol,
+ getCurrencySymbol,
+ isCurrencySymbolLTR,
+ convertToBackendAmount,
+ convertToFrontendAmount,
+ convertToDisplayString,
+ isValidCurrencyCode,
+};
diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js
index b33a1b1b2a73..70c4277bdb5e 100644
--- a/src/libs/DateUtils.js
+++ b/src/libs/DateUtils.js
@@ -298,7 +298,9 @@ function getDateStringFromISOTimestamp(isoTimestamp) {
* @returns {String}
*/
function getStatusUntilDate(inputDate) {
- if (!inputDate) return '';
+ if (!inputDate) {
+ return '';
+ }
const {translateLocal} = Localize;
const input = new Date(inputDate);
diff --git a/src/libs/Log.js b/src/libs/Log.ts
similarity index 70%
rename from src/libs/Log.js
rename to src/libs/Log.ts
index e51fb74aedd5..cf139eec2682 100644
--- a/src/libs/Log.js
+++ b/src/libs/Log.ts
@@ -1,45 +1,43 @@
// Making an exception to this rule here since we don't need an "action" for Log and Log should just be used directly. Creating a Log
// action would likely cause confusion about which one to use. But most other API methods should happen inside an action file.
/* eslint-disable rulesdir/no-api-in-views */
+import {Merge} from 'type-fest';
import Logger from 'expensify-common/lib/Logger';
import getPlatform from './getPlatform';
import pkg from '../../package.json';
import requireParameters from './requireParameters';
import * as Network from './Network';
-let timeout = null;
+let timeout: NodeJS.Timeout;
-/**
- * @param {Object} parameters
- * @param {String} parameters.expensifyCashAppVersion
- * @param {Object[]} parameters.logPacket
- * @returns {Promise}
- */
-function LogCommand(parameters) {
+type LogCommandParameters = {
+ expensifyCashAppVersion: string;
+ logPacket: string;
+};
+
+function LogCommand(parameters: LogCommandParameters): Promise<{requestID: string}> {
const commandName = 'Log';
requireParameters(['logPacket', 'expensifyCashAppVersion'], parameters, commandName);
// Note: We are forcing Log to run since it requires no authToken and should only be queued when we are offline.
// Non-cancellable request: during logout, when requests are cancelled, we don't want to cancel any remaining logs
- return Network.post(commandName, {...parameters, forceNetworkRequest: true, canCancel: false});
+ return Network.post(commandName, {...parameters, forceNetworkRequest: true, canCancel: false}) as Promise<{requestID: string}>;
}
+// eslint-disable-next-line
+type ServerLoggingCallbackOptions = {api_setCookie: boolean; logPacket: string};
+type RequestParams = Merge;
+
/**
* Network interface for logger.
- *
- * @param {Logger} logger
- * @param {Object} params
- * @param {Object} params.parameters
- * @param {String} params.message
- * @return {Promise}
*/
-function serverLoggingCallback(logger, params) {
- const requestParams = params;
+function serverLoggingCallback(logger: Logger, params: ServerLoggingCallbackOptions): Promise<{requestID: string}> {
+ const requestParams = params as RequestParams;
requestParams.shouldProcessImmediately = false;
requestParams.shouldRetry = false;
requestParams.expensifyCashAppVersion = `expensifyCash[${getPlatform()}]${pkg.version}`;
if (requestParams.parameters) {
- requestParams.parameters = JSON.stringify(params.parameters);
+ requestParams.parameters = JSON.stringify(requestParams.parameters);
}
clearTimeout(timeout);
timeout = setTimeout(() => logger.info('Flushing logs older than 10 minutes', true, {}, true), 10 * 60 * 1000);
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index 392781a777db..5c110264e034 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -334,7 +334,7 @@ const NewTeachersUniteNavigator = createModalStackNavigator([
const SaveTheWorldPage = require('../../../pages/TeachersUnite/SaveTheWorldPage').default;
return SaveTheWorldPage;
},
- name: 'SaveTheWorld_Root',
+ name: SCREENS.SAVE_THE_WORLD.ROOT,
},
{
getComponent: () => {
@@ -365,7 +365,7 @@ const SettingsModalStackNavigator = createModalStackNavigator([
const SettingsInitialPage = require('../../../pages/settings/InitialSettingsPage').default;
return SettingsInitialPage;
},
- name: 'Settings_Root',
+ name: SCREENS.SETTINGS.ROOT,
},
{
getComponent: () => {
@@ -506,7 +506,7 @@ const SettingsModalStackNavigator = createModalStackNavigator([
const SettingsSecurityPage = require('../../../pages/settings/Security/SecuritySettingsPage').default;
return SettingsSecurityPage;
},
- name: 'Settings_Security',
+ name: SCREENS.SETTINGS.SECURITY,
},
{
getComponent: () => {
@@ -576,7 +576,7 @@ const SettingsModalStackNavigator = createModalStackNavigator([
const SettingsStatus = require('../../../pages/settings/Profile/CustomStatus/StatusPage').default;
return SettingsStatus;
},
- name: 'Settings_Status',
+ name: SCREENS.SETTINGS.STATUS,
},
{
getComponent: () => {
diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js
index d8cb96e2c6b3..4d50a1cd6a68 100644
--- a/src/libs/Navigation/NavigationRoot.js
+++ b/src/libs/Navigation/NavigationRoot.js
@@ -103,7 +103,8 @@ function NavigationRoot(props) {
prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current;
statusBarBackgroundColor.current = currentScreenBackgroundColor;
- if (prevStatusBarBackgroundColor.current === statusBarBackgroundColor.current) {
+
+ if (currentScreenBackgroundColor === themeColors.appBG && prevStatusBarBackgroundColor.current === themeColors.appBG) {
return;
}
diff --git a/src/libs/Navigation/OnyxTabNavigator.js b/src/libs/Navigation/OnyxTabNavigator.js
index dc68021bf515..2782054497b0 100644
--- a/src/libs/Navigation/OnyxTabNavigator.js
+++ b/src/libs/Navigation/OnyxTabNavigator.js
@@ -33,6 +33,7 @@ function OnyxTabNavigator({id, selectedTab, children, ...rest}) {
id={id}
initialRouteName={selectedTab}
backBehavior="initialRoute"
+ keyboardDismissMode="none"
screenListeners={{
state: (event) => {
const state = event.data.state;
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index 11d21d6d005c..f4420330fbd9 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -38,7 +38,7 @@ export default {
screens: {
Settings: {
screens: {
- Settings_Root: {
+ [SCREENS.SETTINGS.ROOT]: {
path: ROUTES.SETTINGS,
},
[SCREENS.SETTINGS.WORKSPACES]: {
@@ -65,7 +65,7 @@ export default {
path: ROUTES.SETTINGS_CLOSE,
exact: true,
},
- Settings_Security: {
+ [SCREENS.SETTINGS.SECURITY]: {
path: ROUTES.SETTINGS_SECURITY,
exact: true,
},
@@ -159,7 +159,7 @@ export default {
path: ROUTES.SETTINGS_SHARE_CODE,
exact: true,
},
- Settings_Status: {
+ [SCREENS.SETTINGS.STATUS]: {
path: ROUTES.SETTINGS_STATUS,
exact: true,
},
@@ -273,7 +273,7 @@ export default {
},
TeachersUnite: {
screens: {
- SaveTheWorld_Root: ROUTES.TEACHERS_UNITE,
+ [SCREENS.SAVE_THE_WORLD.ROOT]: ROUTES.TEACHERS_UNITE,
I_Know_A_Teacher: ROUTES.I_KNOW_A_TEACHER,
Intro_School_Principal: ROUTES.INTRO_SCHOOL_PRINCIPAL,
I_Am_A_Teacher: ROUTES.I_AM_A_TEACHER,
diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js
new file mode 100644
index 000000000000..0afc8fe10490
--- /dev/null
+++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js
@@ -0,0 +1,15 @@
+import Airship from '@ua/react-native-airship';
+import shouldShowPushNotification from '../shouldShowPushNotification';
+
+function configureForegroundNotifications() {
+ Airship.push.android.setForegroundDisplayPredicate((pushPayload) => Promise.resolve(shouldShowPushNotification(pushPayload)));
+}
+
+function disableForegroundNotifications() {
+ Airship.push.android.setForegroundDisplayPredicate(() => Promise.resolve(false));
+}
+
+export default {
+ configureForegroundNotifications,
+ disableForegroundNotifications,
+};
diff --git a/src/libs/Notification/PushNotification/configureForegroundNotifications/index.ios.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js
similarity index 78%
rename from src/libs/Notification/PushNotification/configureForegroundNotifications/index.ios.js
rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js
index 88d94b4ee805..17ad1baaebe3 100644
--- a/src/libs/Notification/PushNotification/configureForegroundNotifications/index.ios.js
+++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js
@@ -1,7 +1,7 @@
import Airship, {iOS} from '@ua/react-native-airship';
import shouldShowPushNotification from '../shouldShowPushNotification';
-export default function configureForegroundNotifications() {
+function configureForegroundNotifications() {
// Set our default iOS foreground presentation to be loud with a banner
// More info here https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/1649518-usernotificationcenter
Airship.push.iOS.setForegroundPresentationOptions([
@@ -15,3 +15,12 @@ export default function configureForegroundNotifications() {
// Returning null keeps the default presentation. Returning [] uses no presentation (hides the notification).
Airship.push.iOS.setForegroundPresentationOptionsCallback((pushPayload) => Promise.resolve(shouldShowPushNotification(pushPayload) ? null : []));
}
+
+function disableForegroundNotifications() {
+ Airship.push.iOS.setForegroundPresentationOptionsCallback(() => Promise.resolve([]));
+}
+
+export default {
+ configureForegroundNotifications,
+ disableForegroundNotifications,
+};
diff --git a/src/libs/Notification/PushNotification/configureForegroundNotifications/index.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.js
similarity index 52%
rename from src/libs/Notification/PushNotification/configureForegroundNotifications/index.js
rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.js
index c6cb13a0b3b9..acb116f7bc43 100644
--- a/src/libs/Notification/PushNotification/configureForegroundNotifications/index.js
+++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.js
@@ -1,4 +1,7 @@
/**
* Configures notification handling while in the foreground on iOS and Android. This is a no-op on other platforms.
*/
-export default function () {}
+export default {
+ configureForegroundNotifications: () => {},
+ disableForegroundNotifications: () => {},
+};
diff --git a/src/libs/Notification/PushNotification/configureForegroundNotifications/index.android.js b/src/libs/Notification/PushNotification/configureForegroundNotifications/index.android.js
deleted file mode 100644
index 393072df3d12..000000000000
--- a/src/libs/Notification/PushNotification/configureForegroundNotifications/index.android.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import Airship from '@ua/react-native-airship';
-import shouldShowPushNotification from '../shouldShowPushNotification';
-
-export default function configureForegroundNotifications() {
- Airship.push.android.setForegroundDisplayPredicate((pushPayload) => Promise.resolve(shouldShowPushNotification(pushPayload)));
-}
diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.js
index 299af69873f9..7192ee66a791 100644
--- a/src/libs/Notification/PushNotification/index.native.js
+++ b/src/libs/Notification/PushNotification/index.native.js
@@ -6,7 +6,7 @@ import Log from '../../Log';
import NotificationType from './NotificationType';
import * as PushNotification from '../../actions/PushNotification';
import ONYXKEYS from '../../../ONYXKEYS';
-import configureForegroundNotifications from './configureForegroundNotifications';
+import ForegroundNotifications from './ForegroundNotifications';
let isUserOptedInToPushNotifications = false;
Onyx.connect({
@@ -96,7 +96,7 @@ function init() {
// Keep track of which users have enabled push notifications via an NVP.
Airship.addListener(EventType.NotificationOptInStatus, refreshNotificationOptInStatus);
- configureForegroundNotifications();
+ ForegroundNotifications.configureForegroundNotifications();
}
/**
@@ -136,6 +136,7 @@ function deregister() {
Airship.contact.reset();
Airship.removeAllListeners(EventType.PushReceived);
Airship.removeAllListeners(EventType.NotificationResponse);
+ ForegroundNotifications.disableForegroundNotifications();
}
/**
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index e1661e0318b8..3bdf77745432 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -114,6 +114,7 @@ function getPolicyExpenseReportOption(report) {
],
selected: report.selected,
isPolicyExpenseChat: true,
+ searchText: report.searchText,
};
}
@@ -226,6 +227,7 @@ function getParticipantsOption(participant, personalDetails) {
],
phoneNumber: lodashGet(detail, 'phoneNumber', ''),
selected: participant.selected,
+ searchText: participant.searchText,
};
}
@@ -618,7 +620,7 @@ function hasEnabledOptions(options) {
/**
* Build the options for the category tree hierarchy via indents
*
- * @param {Object[]} options - an initial strings array
+ * @param {Object[]} options - an initial object array
* @param {Boolean} options[].enabled - a flag to enable/disable option in a list
* @param {String} options[].name - a name of an option
* @param {Boolean} [isOneLine] - a flag to determine if text should be one line
@@ -775,6 +777,124 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt
return categorySections;
}
+/**
+ * Transforms the provided tags into objects with a specific structure.
+ *
+ * @param {Object[]} tags - an initial tag array
+ * @param {Boolean} tags[].enabled - a flag to enable/disable option in a list
+ * @param {String} tags[].name - a name of an option
+ * @returns {Array
);
@@ -172,7 +195,6 @@ ReceiptSelector.displayName = 'ReceiptSelector';
export default withOnyx({
iou: {key: ONYXKEYS.IOU},
- receiptModal: {key: ONYXKEYS.RECEIPT_MODAL},
report: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '')}`,
},
diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js
index 68e85a34746d..e34730acccea 100644
--- a/src/pages/iou/WaypointEditor.js
+++ b/src/pages/iou/WaypointEditor.js
@@ -23,6 +23,7 @@ import * as Transaction from '../../libs/actions/Transaction';
import * as ValidationUtils from '../../libs/ValidationUtils';
import ROUTES from '../../ROUTES';
import transactionPropTypes from '../../components/transactionPropTypes';
+import * as ErrorUtils from '../../libs/ErrorUtils';
const propTypes = {
/** The transactionID of the IOU */
@@ -104,18 +105,28 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI
const errors = {};
const waypointValue = values[`waypoint${waypointIndex}`] || '';
if (isOffline && waypointValue !== '' && !ValidationUtils.isValidAddress(waypointValue)) {
- errors[`waypoint${waypointIndex}`] = 'bankAccount.error.address';
+ ErrorUtils.addErrorMessage(errors, `waypoint${waypointIndex}`, 'bankAccount.error.address');
}
// If the user is online and they are trying to save a value without using the autocomplete, show an error message instructing them to use a selected address instead.
// That enables us to save the address with coordinates when it is selected
if (!isOffline && waypointValue !== '' && waypointAddress !== waypointValue) {
- errors[`waypoint${waypointIndex}`] = 'distance.errors.selectSuggestedAddress';
+ ErrorUtils.addErrorMessage(errors, `waypoint${waypointIndex}`, 'distance.errors.selectSuggestedAddress');
}
return errors;
};
+ const saveWaypoint = (waypoint) => {
+ if (parsedWaypointIndex < _.size(allWaypoints)) {
+ Transaction.saveWaypoint(transactionID, waypointIndex, waypoint);
+ } else {
+ const finishWaypoint = lodashGet(allWaypoints, `waypoint${_.size(allWaypoints) - 1}`, {});
+ Transaction.saveWaypoint(transactionID, waypointIndex, finishWaypoint);
+ Transaction.saveWaypoint(transactionID, waypointIndex - 1, waypoint);
+ }
+ };
+
const onSubmit = (values) => {
const waypointValue = values[`waypoint${waypointIndex}`] || '';
@@ -132,8 +143,7 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI
lng: null,
address: waypointValue,
};
-
- Transaction.saveWaypoint(transactionID, waypointIndex, waypoint);
+ saveWaypoint(waypoint);
}
// Other flows will be handled by selecting a waypoint with selectWaypoint as this is mainly for the offline flow
@@ -152,8 +162,7 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI
lng: values.lng,
address: values.address,
};
-
- Transaction.saveWaypoint(transactionID, waypointIndex, waypoint);
+ saveWaypoint(waypoint);
Navigation.goBack(ROUTES.getMoneyRequestDistanceTabRoute(iouType));
};
@@ -163,7 +172,7 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI
onEntryTransitionEnd={() => textInput.current && textInput.current.focus()}
shouldEnableMaxHeight
>
- waypointCount - 1) && isFocused}>
+ waypointCount) && isFocused}>
(textInput.current = e)}
- hint={!isOffline ? translate('distance.errors.selectSuggestedAddress') : ''}
+ hint={!isOffline ? 'distance.errors.selectSuggestedAddress' : ''}
containerStyles={[styles.mt4]}
label={translate('distance.address')}
defaultValue={waypointAddress}
diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js
index 1ea0b002b235..e08fd5bde881 100644
--- a/src/pages/iou/steps/MoneyRequestAmountForm.js
+++ b/src/pages/iou/steps/MoneyRequestAmountForm.js
@@ -12,6 +12,7 @@ import * as DeviceCapabilities from '../../../libs/DeviceCapabilities';
import TextInputWithCurrencySymbol from '../../../components/TextInputWithCurrencySymbol';
import useLocalize from '../../../hooks/useLocalize';
import CONST from '../../../CONST';
+import FormHelpMessage from '../../../components/FormHelpMessage';
import refPropTypes from '../../../components/refPropTypes';
import getOperatingSystem from '../../../libs/getOperatingSystem';
import * as Browser from '../../../libs/Browser';
@@ -57,6 +58,8 @@ const getNewSelection = (oldSelection, prevLength, newLength) => {
return {start: cursorPosition, end: cursorPosition};
};
+const isAmountValid = (amount) => !amount.length || parseFloat(amount) < 0.01;
+
const AMOUNT_VIEW_ID = 'amountView';
const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView';
const NUM_PAD_VIEW_ID = 'numPadView';
@@ -70,6 +73,9 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
const selectedAmountAsString = amount ? CurrencyUtils.convertToFrontendAmount(amount).toString() : '';
const [currentAmount, setCurrentAmount] = useState(selectedAmountAsString);
+ const [isInvalidAmount, setIsInvalidAmount] = useState(isAmountValid(selectedAmountAsString));
+ const [firstPress, setFirstPress] = useState(false);
+ const [formError, setFormError] = useState('');
const [shouldUpdateSelection, setShouldUpdateSelection] = useState(true);
const [selection, setSelection] = useState({
@@ -127,6 +133,9 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
setSelection((prevSelection) => ({...prevSelection}));
return;
}
+ const checkInvalidAmount = isAmountValid(newAmountWithoutSpaces);
+ setIsInvalidAmount(checkInvalidAmount);
+ setFormError(checkInvalidAmount ? 'iou.error.invalidAmount' : '');
setCurrentAmount((prevAmount) => {
const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(newAmountWithoutSpaces);
const isForwardDelete = prevAmount.length > strippedAmount.length && forwardDeletePressedRef.current;
@@ -177,8 +186,13 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
* Submit amount and navigate to a proper page
*/
const submitAndNavigateToNextPage = useCallback(() => {
+ if (isInvalidAmount) {
+ setFirstPress(true);
+ setFormError('iou.error.invalidAmount');
+ return;
+ }
onSubmitButtonPress(currentAmount);
- }, [onSubmitButtonPress, currentAmount]);
+ }, [onSubmitButtonPress, currentAmount, isInvalidAmount]);
/**
* Input handler to check for a forward-delete key (or keyboard shortcut) press.
@@ -231,9 +245,16 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
onKeyPress={textInputKeyPress}
/>
+ {!_.isEmpty(formError) && firstPress && (
+
+ )}
onMouseDown(event, [NUM_PAD_CONTAINER_VIEW_ID, NUM_PAD_VIEW_ID])}
- style={[styles.w100, styles.justifyContentEnd, styles.pageWrapper]}
+ style={[styles.w100, styles.justifyContentEnd, styles.pageWrapper, styles.pt0]}
nativeID={NUM_PAD_CONTAINER_VIEW_ID}
>
{DeviceCapabilities.canUseTouchScreen() ? (
@@ -249,7 +270,6 @@ function MoneyRequestAmountForm({amount, currency, isEditing, forwardedRef, onCu
style={[styles.w100, styles.mt5]}
onPress={submitAndNavigateToNextPage}
pressOnEnter
- isDisabled={!currentAmount.length || parseFloat(currentAmount) < 0.01}
text={buttonText}
/>
diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js
index 224f661915d8..93ee2c7f8aac 100644
--- a/src/pages/iou/steps/MoneyRequestConfirmPage.js
+++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js
@@ -141,6 +141,7 @@ function MoneyRequestConfirmPage(props) {
trimmedComment,
receipt,
props.iou.category,
+ props.iou.tag,
props.iou.billable,
);
},
@@ -153,6 +154,7 @@ function MoneyRequestConfirmPage(props) {
props.currentUserPersonalDetails.login,
props.currentUserPersonalDetails.accountID,
props.iou.category,
+ props.iou.tag,
props.iou.billable,
],
);
@@ -170,12 +172,13 @@ function MoneyRequestConfirmPage(props) {
props.iou.created,
props.iou.transactionID,
props.iou.category,
+ props.iou.tag,
props.iou.amount,
props.iou.currency,
props.iou.merchant,
);
},
- [props.report, props.iou.created, props.iou.transactionID, props.iou.category, props.iou.amount, props.iou.currency, props.iou.merchant],
+ [props.report, props.iou.created, props.iou.transactionID, props.iou.category, props.iou.tag, props.iou.amount, props.iou.currency, props.iou.merchant],
);
const createTransaction = useCallback(
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
index cb9f4cdd9b7f..b9ee016d4099 100644
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js
@@ -63,7 +63,19 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
setHeaderTitle(_.isEmpty(iou.participants) ? translate('tabSelector.manual') : translate('iou.split'));
}, [iou.participants, isDistanceRequest, translate]);
- const navigateToNextStep = (moneyRequestType) => {
+ const navigateToRequestStep = (moneyRequestType, option) => {
+ if (option.reportID) {
+ isNewReportIDSelectedLocally.current = true;
+ IOU.setMoneyRequestId(`${moneyRequestType}${option.reportID}`);
+ Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(moneyRequestType, option.reportID));
+ return;
+ }
+
+ IOU.setMoneyRequestId(moneyRequestType);
+ Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(moneyRequestType, reportID.current));
+ };
+
+ const navigateToSplitStep = (moneyRequestType) => {
IOU.setMoneyRequestId(moneyRequestType);
Navigation.navigate(ROUTES.getMoneyRequestConfirmationRoute(moneyRequestType, reportID.current));
};
@@ -113,8 +125,8 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route}) {
ref={(el) => (optionsSelectorRef.current = el)}
participants={iou.participants}
onAddParticipants={IOU.setMoneyRequestParticipants}
- navigateToRequest={() => navigateToNextStep(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)}
- navigateToSplit={() => navigateToNextStep(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)}
+ navigateToRequest={(option) => navigateToRequestStep(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, option)}
+ navigateToSplit={() => navigateToSplitStep(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT)}
safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle}
iouType={iouType.current}
isDistanceRequest={isDistanceRequest}
diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
index 4f761e92eaf5..170ee042bffa 100755
--- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
+++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js
@@ -158,8 +158,10 @@ function MoneyRequestParticipantsSelector({
* @param {Object} option
*/
const addSingleParticipant = (option) => {
- onAddParticipants([{accountID: option.accountID, login: option.login, isPolicyExpenseChat: option.isPolicyExpenseChat, reportID: option.reportID, selected: true}]);
- navigateToRequest();
+ onAddParticipants([
+ {accountID: option.accountID, login: option.login, isPolicyExpenseChat: option.isPolicyExpenseChat, reportID: option.reportID, selected: true, searchText: option.searchText},
+ ]);
+ navigateToRequest(option);
};
/**
@@ -187,13 +189,20 @@ function MoneyRequestParticipantsSelector({
} else {
newSelectedOptions = [
...participants,
- {accountID: option.accountID, login: option.login, isPolicyExpenseChat: option.isPolicyExpenseChat, reportID: option.reportID, selected: true},
+ {
+ accountID: option.accountID,
+ login: option.login,
+ isPolicyExpenseChat: option.isPolicyExpenseChat,
+ reportID: option.reportID,
+ selected: true,
+ searchText: option.searchText,
+ },
];
}
onAddParticipants(newSelectedOptions);
- const chatOptions = OptionsListUtils.getNewChatOptions(
+ const chatOptions = OptionsListUtils.getFilteredOptions(
reports,
personalDetails,
betas,
@@ -223,12 +232,12 @@ function MoneyRequestParticipantsSelector({
Boolean(newChatOptions.userToInvite),
searchTerm.trim(),
maxParticipantsReached,
- _.some(participants, (participant) => participant.login.toLowerCase().includes(searchTerm.trim().toLowerCase())),
+ _.some(participants, (participant) => participant.searchText.toLowerCase().includes(searchTerm.trim().toLowerCase())),
);
const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails);
useEffect(() => {
- const chatOptions = OptionsListUtils.getNewChatOptions(
+ const chatOptions = OptionsListUtils.getFilteredOptions(
reports,
personalDetails,
betas,
diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.js
index e703c7f2f24f..6e44d8d57e32 100644
--- a/src/pages/iou/steps/NewRequestAmountPage.js
+++ b/src/pages/iou/steps/NewRequestAmountPage.js
@@ -67,7 +67,7 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) {
const currentCurrency = lodashGet(route, 'params.currency', '');
const isDistanceRequestTab = MoneyRequestUtils.isDistanceRequest(iouType, selectedTab);
- const currency = currentCurrency || iou.currency;
+ const currency = CurrencyUtils.isValidCurrencyCode(currentCurrency) ? currentCurrency : iou.currency;
const focusTextInput = () => {
// Component may not be initialized due to navigation transitions
diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js
index a67e7cbc122e..d10779210b09 100755
--- a/src/pages/settings/InitialSettingsPage.js
+++ b/src/pages/settings/InitialSettingsPage.js
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import React, {useState, useEffect, useRef, useMemo, useCallback} from 'react';
-import {View, ScrollView} from 'react-native';
+import {View} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
@@ -12,11 +12,11 @@ import * as Session from '../../libs/actions/Session';
import ONYXKEYS from '../../ONYXKEYS';
import Tooltip from '../../components/Tooltip';
import Avatar from '../../components/Avatar';
-import HeaderWithBackButton from '../../components/HeaderWithBackButton';
import Navigation from '../../libs/Navigation/Navigation';
import * as Expensicons from '../../components/Icon/Expensicons';
-import ScreenWrapper from '../../components/ScreenWrapper';
import MenuItem from '../../components/MenuItem';
+import themeColors from '../../styles/themes/default';
+import SCREENS from '../../SCREENS';
import ROUTES from '../../ROUTES';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import compose from '../../libs/compose';
@@ -43,6 +43,7 @@ import PressableWithoutFeedback from '../../components/Pressable/PressableWithou
import useLocalize from '../../hooks/useLocalize';
import useSingleExecution from '../../hooks/useSingleExecution';
import useWaitForNavigation from '../../hooks/useWaitForNavigation';
+import HeaderPageLayout from '../../components/HeaderPageLayout';
const propTypes = {
/* Onyx Props */
@@ -326,79 +327,80 @@ function InitialSettingsPage(props) {
if (_.isEmpty(props.currentUserPersonalDetails)) {
return null;
}
-
- return (
-
- {({safeAreaPaddingBottomStyle}) => (
+ const headerContent = (
+
+ {_.isEmpty(props.currentUserPersonalDetails) || _.isUndefined(props.currentUserPersonalDetails.displayName) ? (
+
+ ) : (
<>
-
-
+
+
+
+
+
+
+
-
- {_.isEmpty(props.currentUserPersonalDetails) || _.isUndefined(props.currentUserPersonalDetails.displayName) ? (
-
- ) : (
-
-
-
-
-
-
-
-
-
-
-
- {props.currentUserPersonalDetails.displayName ? props.currentUserPersonalDetails.displayName : props.formatPhoneNumber(props.session.email)}
-
-
-
- {Boolean(props.currentUserPersonalDetails.displayName) && (
-
- {props.formatPhoneNumber(props.session.email)}
-
- )}
-
- )}
- {getMenuItems}
-
- signOut(true)}
- onCancel={() => toggleSignoutConfirmModal(false)}
- />
-
-
+
+
+ {props.currentUserPersonalDetails.displayName ? props.currentUserPersonalDetails.displayName : props.formatPhoneNumber(props.session.email)}
+
+
+
+ {Boolean(props.currentUserPersonalDetails.displayName) && (
+
+ {props.formatPhoneNumber(props.session.email)}
+
+ )}
>
)}
-
+
+ );
+
+ return (
+
+
+ {getMenuItems}
+ signOut(true)}
+ onCancel={() => toggleSignoutConfirmModal(false)}
+ />
+
+
);
}
diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.js b/src/pages/settings/Profile/CustomStatus/StatusPage.js
index dcd356978bd3..807bd73cecc1 100644
--- a/src/pages/settings/Profile/CustomStatus/StatusPage.js
+++ b/src/pages/settings/Profile/CustomStatus/StatusPage.js
@@ -4,7 +4,7 @@ import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '../../../../components/withCurrentUserPersonalDetails';
import MenuItemWithTopDescription from '../../../../components/MenuItemWithTopDescription';
-import StaticHeaderPageLayout from '../../../../components/StaticHeaderPageLayout';
+import HeaderPageLayout from '../../../../components/HeaderPageLayout';
import * as Expensicons from '../../../../components/Icon/Expensicons';
import withLocalize from '../../../../components/withLocalize';
import MenuItem from '../../../../components/MenuItem';
@@ -19,6 +19,7 @@ import styles from '../../../../styles/styles';
import compose from '../../../../libs/compose';
import ONYXKEYS from '../../../../ONYXKEYS';
import ROUTES from '../../../../ROUTES';
+import SCREENS from '../../../../SCREENS';
const propTypes = {
...withCurrentUserPersonalDetailsPropTypes,
@@ -63,11 +64,17 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
useEffect(() => () => User.clearDraftCustomStatus(), []);
return (
-
+ }
+ headerContainerStyles={[styles.staticHeaderImage]}
+ backgroundColor={themeColors.PAGE_BACKGROUND_COLORS[SCREENS.SETTINGS.STATUS]}
footer={footerComponent}
>
@@ -91,7 +98,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
wrapperStyle={[styles.cardMenuItem]}
/>
)}
-
+
);
}
diff --git a/src/pages/settings/Security/SecuritySettingsPage.js b/src/pages/settings/Security/SecuritySettingsPage.js
index 7f08247557f4..293e488ede7a 100644
--- a/src/pages/settings/Security/SecuritySettingsPage.js
+++ b/src/pages/settings/Security/SecuritySettingsPage.js
@@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
+import SCREENS from '../../../SCREENS';
import styles from '../../../styles/styles';
import * as Expensicons from '../../../components/Icon/Expensicons';
import themeColors from '../../../styles/themes/default';
@@ -54,7 +55,7 @@ function SecuritySettingsPage(props) {
shouldShowBackButton
shouldShowCloseButton
illustration={LottieAnimations.Safe}
- backgroundColor={themeColors.PAGE_BACKGROUND_COLORS[ROUTES.SETTINGS_SECURITY]}
+ backgroundColor={themeColors.PAGE_BACKGROUND_COLORS[SCREENS.SETTINGS.SECURITY]}
>
diff --git a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js
index 52d7a9806f69..7aa7a8ab64c1 100644
--- a/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js
+++ b/src/pages/settings/Security/TwoFactorAuth/Steps/CodesStep.js
@@ -1,4 +1,4 @@
-import React, {useEffect} from 'react';
+import React, {useEffect, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import {ActivityIndicator, View} from 'react-native';
import {ScrollView} from 'react-native-gesture-handler';
@@ -23,10 +23,12 @@ import useWindowDimensions from '../../../../../hooks/useWindowDimensions';
import StepWrapper from '../StepWrapper/StepWrapper';
import {defaultAccount, TwoFactorAuthPropTypes} from '../TwoFactorAuthPropTypes';
import * as TwoFactorAuthActions from '../../../../../libs/actions/TwoFactorAuthActions';
+import FormHelpMessage from '../../../../../components/FormHelpMessage';
function CodesStep({account = defaultAccount}) {
const {translate} = useLocalize();
const {isExtraSmallScreenWidth, isSmallScreenWidth} = useWindowDimensions();
+ const [error, setError] = useState('');
const {setStep} = useTwoFactorAuthContext();
@@ -83,6 +85,7 @@ function CodesStep({account = defaultAccount}) {
inline={false}
onPress={() => {
Clipboard.setString(account.recoveryCodes);
+ setError('');
TwoFactorAuthActions.setCodesAreCopied();
}}
styles={[styles.button, styles.buttonMedium, styles.twoFactorAuthCodesButton]}
@@ -93,6 +96,7 @@ function CodesStep({account = defaultAccount}) {
icon={Expensicons.Download}
onPress={() => {
localFileDownload('two-factor-auth-codes', account.recoveryCodes);
+ setError('');
TwoFactorAuthActions.setCodesAreCopied();
}}
inline={false}
@@ -106,11 +110,23 @@ function CodesStep({account = defaultAccount}) {
+ {!_.isEmpty(error) && (
+
+ )}
diff --git a/src/pages/settings/Wallet/WalletPage/BaseWalletPage.js b/src/pages/settings/Wallet/WalletPage/BaseWalletPage.js
index be40ef1af495..b416ded298e5 100644
--- a/src/pages/settings/Wallet/WalletPage/BaseWalletPage.js
+++ b/src/pages/settings/Wallet/WalletPage/BaseWalletPage.js
@@ -74,7 +74,9 @@ function BaseWalletPage(props) {
* @param {Object} position
*/
const setMenuPosition = useCallback(() => {
- if (!paymentMethodButtonRef.current) return;
+ if (!paymentMethodButtonRef.current) {
+ return;
+ }
const position = getClickedTargetLocation(paymentMethodButtonRef.current);
diff --git a/src/pages/tasks/NewTaskPage.js b/src/pages/tasks/NewTaskPage.js
index 9fb600e40753..e4aa0bcb08e1 100644
--- a/src/pages/tasks/NewTaskPage.js
+++ b/src/pages/tasks/NewTaskPage.js
@@ -126,7 +126,15 @@ function NewTaskPage(props) {
return;
}
- Task.createTaskAndNavigate(parentReport.reportID, props.task.title, props.task.description, props.task.assignee, props.task.assigneeAccountID, props.task.assigneeChatReport);
+ Task.createTaskAndNavigate(
+ parentReport.reportID,
+ props.task.title,
+ props.task.description,
+ props.task.assignee,
+ props.task.assigneeAccountID,
+ props.task.assigneeChatReport,
+ parentReport.policyID,
+ );
}
if (!Permissions.canUseTasks(props.betas)) {
diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js
index c4dd2dee2d8c..2e7ddea1926d 100644
--- a/src/pages/tasks/TaskAssigneeSelectorModal.js
+++ b/src/pages/tasks/TaskAssigneeSelectorModal.js
@@ -83,7 +83,7 @@ function TaskAssigneeSelectorModal(props) {
const optionRef = useRef();
const updateOptions = useCallback(() => {
- const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getNewChatOptions(
+ const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions(
props.reports,
props.personalDetails,
props.betas,
@@ -96,6 +96,9 @@ function TaskAssigneeSelectorModal(props) {
{},
[],
false,
+ {},
+ [],
+ false,
);
setHeaderMessage(OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), searchValue));
diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.js
index 9a7a36a1e119..be0884e971ae 100644
--- a/src/pages/tasks/TaskTitlePage.js
+++ b/src/pages/tasks/TaskTitlePage.js
@@ -100,7 +100,9 @@ function TaskTitlePage(props) {
accessibilityLabel={props.translate('task.title')}
defaultValue={(props.report && props.report.reportName) || ''}
ref={(el) => {
- if (!el) return;
+ if (!el) {
+ return;
+ }
if (!inputRef.current && didScreenTransitionEnd) {
el.focus();
}
diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts
index ec06bb07c3fe..11ade02220ab 100644
--- a/src/styles/StyleUtils.ts
+++ b/src/styles/StyleUtils.ts
@@ -25,6 +25,7 @@ type AvatarSizeValue = ValueOf<
| 'avatarSizeSubscript'
| 'avatarSizeSmall'
| 'avatarSizeSmaller'
+ | 'avatarSizeXLarge'
| 'avatarSizeLarge'
| 'avatarSizeMedium'
| 'avatarSizeLargeBordered'
@@ -95,6 +96,7 @@ const avatarBorderSizes: Partial> = {
[CONST.AVATAR_SIZE.DEFAULT]: variables.componentBorderRadiusNormal,
[CONST.AVATAR_SIZE.MEDIUM]: variables.componentBorderRadiusLarge,
[CONST.AVATAR_SIZE.LARGE]: variables.componentBorderRadiusLarge,
+ [CONST.AVATAR_SIZE.XLARGE]: variables.componentBorderRadiusLarge,
[CONST.AVATAR_SIZE.LARGE_BORDERED]: variables.componentBorderRadiusRounded,
[CONST.AVATAR_SIZE.SMALL_NORMAL]: variables.componentBorderRadiusMedium,
};
@@ -107,6 +109,7 @@ const avatarSizes: Record = {
[CONST.AVATAR_SIZE.SMALL]: variables.avatarSizeSmall,
[CONST.AVATAR_SIZE.SMALLER]: variables.avatarSizeSmaller,
[CONST.AVATAR_SIZE.LARGE]: variables.avatarSizeLarge,
+ [CONST.AVATAR_SIZE.XLARGE]: variables.avatarSizeXLarge,
[CONST.AVATAR_SIZE.MEDIUM]: variables.avatarSizeMedium,
[CONST.AVATAR_SIZE.LARGE_BORDERED]: variables.avatarSizeLargeBordered,
[CONST.AVATAR_SIZE.HEADER]: variables.avatarSizeHeader,
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 01689a43952a..4a2472913fd2 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -879,7 +879,8 @@ const styles = (theme) => ({
offlineIndicatorMobile: {
paddingLeft: 20,
paddingTop: 5,
- paddingBottom: 5,
+ paddingBottom: 30,
+ marginBottom: -25,
},
offlineIndicatorRow: {
@@ -1994,6 +1995,11 @@ const styles = (theme) => ({
height: variables.avatarSizeLarge,
},
+ avatarXLarge: {
+ width: variables.avatarSizeXLarge,
+ height: variables.avatarSizeXLarge,
+ },
+
avatarNormal: {
height: variables.componentSizeNormal,
width: variables.componentSizeNormal,
diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js
index 6abc8d7e4463..5ff997684304 100644
--- a/src/styles/themes/default.js
+++ b/src/styles/themes/default.js
@@ -87,11 +87,12 @@ const darkTheme = {
darkTheme.PAGE_BACKGROUND_COLORS = {
[SCREENS.HOME]: darkTheme.sidebar,
+ [SCREENS.SAVE_THE_WORLD.ROOT]: colors.tangerine800,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
- [ROUTES.SETTINGS_STATUS]: colors.green700,
- [ROUTES.I_KNOW_A_TEACHER]: colors.tangerine800,
- [ROUTES.SETTINGS_SECURITY]: colors.ice500,
+ [SCREENS.SETTINGS.SECURITY]: colors.ice500,
+ [SCREENS.SETTINGS.STATUS]: colors.green700,
+ [SCREENS.SETTINGS.ROOT]: darkTheme.sidebar,
};
export default darkTheme;
diff --git a/src/styles/themes/light.js b/src/styles/themes/light.js
index 69127e19ae14..e6ca56d248d6 100644
--- a/src/styles/themes/light.js
+++ b/src/styles/themes/light.js
@@ -1,6 +1,5 @@
import colors from '../colors';
import SCREENS from '../../SCREENS';
-import ROUTES from '../../ROUTES';
const lightTheme = {
// Figma keys
@@ -86,11 +85,12 @@ const lightTheme = {
lightTheme.PAGE_BACKGROUND_COLORS = {
[SCREENS.HOME]: lightTheme.sidebar,
+ [SCREENS.SAVE_THE_WORLD.ROOT]: colors.tangerine800,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
- [ROUTES.SETTINGS_STATUS]: colors.green700,
- [ROUTES.I_KNOW_A_TEACHER]: colors.tangerine800,
- [ROUTES.SETTINGS_SECURITY]: colors.ice500,
+ [SCREENS.SETTINGS.SECURITY]: colors.ice500,
+ [SCREENS.SETTINGS.STATUS]: colors.green700,
+ [SCREENS.SETTINGS.ROOT]: lightTheme.sidebar,
};
export default lightTheme;
diff --git a/src/styles/themes/useThemePreference.js b/src/styles/themes/useThemePreference.js
index fbb557423f10..579e9dc97c69 100644
--- a/src/styles/themes/useThemePreference.js
+++ b/src/styles/themes/useThemePreference.js
@@ -18,8 +18,11 @@ function useThemePreference() {
const theme = preferredThemeContext || CONST.THEME.DEFAULT;
// If the user chooses to use the device theme settings, we need to set the theme preference to the system theme
- if (theme === CONST.THEME.SYSTEM) setThemePreference(systemTheme);
- else setThemePreference(theme);
+ if (theme === CONST.THEME.SYSTEM) {
+ setThemePreference(systemTheme);
+ } else {
+ setThemePreference(theme);
+ }
}, [preferredThemeContext, systemTheme]);
return themePreference;
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index 6291323cef0b..c75d03a39986 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -28,6 +28,7 @@ export default {
appModalAppIconSize: 48,
buttonBorderRadius: 100,
avatarSizeLargeBordered: 88,
+ avatarSizeXLarge: 120,
avatarSizeLarge: 80,
avatarSizeMedium: 52,
avatarSizeHeader: 40,
diff --git a/src/types/onyx/ReceiptModal.ts b/src/types/onyx/ReceiptModal.ts
deleted file mode 100644
index 0d52f684b4d2..000000000000
--- a/src/types/onyx/ReceiptModal.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-type ReceiptModal = {
- isAttachmentInvalid: boolean;
- attachmentInvalidReasonTitle: string;
- attachmentInvalidReason: string;
-};
-
-export default ReceiptModal;
diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts
index 5f0072959917..ec505a7e8d07 100644
--- a/src/types/onyx/ReportAction.ts
+++ b/src/types/onyx/ReportAction.ts
@@ -46,6 +46,9 @@ type ReportActionBase = {
/** The ID of the reportAction. It is the string representation of the a 64-bit integer. */
reportActionID?: string;
+ /** The ID of the previous reportAction on the report. It is a string represenation of a 64-bit integer (or null for CREATED actions). */
+ previousReportActionID?: string;
+
actorAccountID?: number;
/** Person who created the action */
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index de128d85e6b1..ea0b178444b5 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -19,25 +19,27 @@ type Route = {
type Routes = Record;
type Transaction = {
- transactionID: string;
amount: number;
category: string;
- currency: string;
- reportID: string;
comment: Comment;
- merchant: string;
created: string;
- pendingAction: OnyxCommon.PendingAction;
+ currency: string;
errors: OnyxCommon.Errors;
+ // The name of the file used for a receipt (formerly receiptFilename)
+ filename?: string;
+ merchant: string;
modifiedAmount?: number;
modifiedCreated?: string;
modifiedCurrency?: string;
+ pendingAction: OnyxCommon.PendingAction;
receipt: {
receiptID?: number;
source?: string;
state?: ValueOf;
};
+ reportID: string;
routes?: Routes;
+ transactionID: string;
tag: string;
};
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 8711a0d208ef..069909153096 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -29,7 +29,6 @@ import FrequentlyUsedEmoji from './FrequentlyUsedEmoji';
import ReimbursementAccount from './ReimbursementAccount';
import ReimbursementAccountDraft from './ReimbursementAccountDraft';
import WalletTransfer from './WalletTransfer';
-import ReceiptModal from './ReceiptModal';
import MapboxAccessToken from './MapboxAccessToken';
import {OnyxUpdatesFromServer, OnyxUpdateEvent} from './OnyxUpdatesFromServer';
import Download from './Download';
@@ -79,7 +78,6 @@ export type {
ReimbursementAccountDraft,
FrequentlyUsedEmoji,
WalletTransfer,
- ReceiptModal,
MapboxAccessToken,
Download,
PolicyMember,
diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js
index 01c2b4711ce7..7372bb76b9e5 100644
--- a/tests/actions/IOUTest.js
+++ b/tests/actions/IOUTest.js
@@ -11,6 +11,19 @@ import * as ReportActions from '../../src/libs/actions/ReportActions';
import * as Report from '../../src/libs/actions/Report';
import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager';
import waitForNetworkPromises from '../utils/waitForNetworkPromises';
+import * as ReportUtils from '../../src/libs/ReportUtils';
+import * as ReportActionsUtils from '../../src/libs/ReportActionsUtils';
+import * as PersonalDetailsUtils from '../../src/libs/PersonalDetailsUtils';
+import * as User from '../../src/libs/actions/User';
+import PusherHelper from '../utils/PusherHelper';
+import Navigation from '../../src/libs/Navigation/Navigation';
+import ROUTES from '../../src/ROUTES';
+
+jest.mock('../../src/libs/Navigation/Navigation', () => ({
+ navigate: jest.fn(),
+ dismissModal: jest.fn(),
+ goBack: jest.fn(),
+}));
const CARLOS_EMAIL = 'cmartins@expensifail.com';
const CARLOS_ACCOUNT_ID = 1;
@@ -1376,4 +1389,773 @@ describe('actions/IOU', () => {
);
});
});
+
+ describe('deleteMoneyRequest', () => {
+ const amount = 10000;
+ const comment = 'Send me money please';
+ let chatReport;
+ let iouReport;
+ let createIOUAction;
+ let transaction;
+ let thread;
+ const TEST_USER_ACCOUNT_ID = 1;
+ const TEST_USER_LOGIN = 'test@test.com';
+ let IOU_REPORT_ID;
+ let reportActionID;
+ const REPORT_ACTION = {
+ actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT,
+ actorAccountID: TEST_USER_ACCOUNT_ID,
+ automatic: false,
+ avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png',
+ message: [{type: 'COMMENT', html: 'Testing a comment', text: 'Testing a comment', translationKey: ''}],
+ person: [{type: 'TEXT', style: 'strong', text: 'Test User'}],
+ shouldShow: true,
+ };
+
+ let reportActions;
+
+ beforeEach(async () => {
+ // Given mocks are cleared and helpers are set up
+ jest.clearAllMocks();
+ PusherHelper.setup();
+
+ // Given a test user is signed in with Onyx setup and some initial data
+ await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN);
+ User.subscribeToUserEvents();
+ await waitForBatchedUpdates();
+ await TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID);
+
+ // When an IOU request for money is made
+ IOU.requestMoney({}, amount, CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment);
+ await waitForBatchedUpdates();
+
+ // When fetching all reports from Onyx
+ const allReports = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (reports) => {
+ Onyx.disconnect(connectionID);
+ resolve(reports);
+ },
+ });
+ });
+
+ // Then we should have exactly 2 reports
+ expect(_.size(allReports)).toBe(2);
+
+ // Then one of them should be a chat report with relevant properties
+ chatReport = _.find(allReports, (report) => report.type === CONST.REPORT.TYPE.CHAT);
+ expect(chatReport).toBeTruthy();
+ expect(chatReport).toHaveProperty('reportID');
+ expect(chatReport).toHaveProperty('iouReportID');
+ expect(chatReport.hasOutstandingIOU).toBe(true);
+
+ // Then one of them should be an IOU report with relevant properties
+ iouReport = _.find(allReports, (report) => report.type === CONST.REPORT.TYPE.IOU);
+ expect(iouReport).toBeTruthy();
+ expect(iouReport).toHaveProperty('reportID');
+ expect(iouReport).toHaveProperty('chatReportID');
+
+ // Then their IDs should reference each other
+ expect(chatReport.iouReportID).toBe(iouReport.reportID);
+ expect(iouReport.chatReportID).toBe(chatReport.reportID);
+
+ // Storing IOU Report ID for further reference
+ IOU_REPORT_ID = chatReport.iouReportID;
+
+ await waitForBatchedUpdates();
+
+ // When fetching all report actions from Onyx
+ const allReportActions = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
+ waitForCollectionCallback: true,
+ callback: (actions) => {
+ Onyx.disconnect(connectionID);
+ resolve(actions);
+ },
+ });
+ });
+
+ // Then we should find an IOU action with specific properties
+ const reportActionsForIOUReport = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.iouReportID}`];
+ createIOUAction = _.find(reportActionsForIOUReport, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ expect(createIOUAction).toBeTruthy();
+ expect(createIOUAction.originalMessage.IOUReportID).toBe(iouReport.reportID);
+
+ // When fetching all transactions from Onyx
+ const allTransactions = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.TRANSACTION,
+ waitForCollectionCallback: true,
+ callback: (transactions) => {
+ Onyx.disconnect(connectionID);
+ resolve(transactions);
+ },
+ });
+ });
+
+ // Then we should find a specific transaction with relevant properties
+ transaction = _.find(allTransactions, (t) => t);
+ expect(transaction).toBeTruthy();
+ expect(transaction.amount).toBe(amount);
+ expect(transaction.reportID).toBe(iouReport.reportID);
+ expect(createIOUAction.originalMessage.IOUTransactionID).toBe(transaction.transactionID);
+ });
+
+ afterEach(PusherHelper.teardown);
+
+ it('delete a money request (IOU Action and transaction) successfully', async () => {
+ // Given the fetch operations are paused and a money request is initiated
+ fetch.pause();
+
+ // When the money request is deleted
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, true);
+ await waitForBatchedUpdates();
+
+ // Then we check if the IOU report action is removed from the report actions collection
+ let reportActionsForReport = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (actionsForReport) => {
+ Onyx.disconnect(connectionID);
+ resolve(actionsForReport);
+ },
+ });
+ });
+
+ createIOUAction = _.find(reportActionsForReport, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ expect(createIOUAction).toBeFalsy();
+
+ // Then we check if the transaction is removed from the transactions collection
+ const t = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ waitForCollectionCallback: true,
+ callback: (transactionResult) => {
+ Onyx.disconnect(connectionID);
+ resolve(transactionResult);
+ },
+ });
+ });
+
+ expect(t).toBeFalsy();
+
+ // Given fetch operations are resumed
+ fetch.resume();
+
+ // Then we recheck the IOU report action from the report actions collection
+ reportActionsForReport = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (actionsForReport) => {
+ Onyx.disconnect(connectionID);
+ resolve(actionsForReport);
+ },
+ });
+ });
+
+ createIOUAction = _.find(reportActionsForReport, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ expect(createIOUAction).toBeFalsy();
+
+ // Then we recheck the transaction from the transactions collection
+ const tr = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ waitForCollectionCallback: true,
+ callback: (transactionResult) => {
+ Onyx.disconnect(connectionID);
+ resolve(transactionResult);
+ },
+ });
+ });
+
+ expect(tr).toBeFalsy();
+ });
+
+ it('delete the IOU report when there are no visible comments left in the IOU report', async () => {
+ // Given an IOU report and a paused fetch state
+ fetch.pause();
+
+ // When the IOU money request is deleted
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, true);
+ await waitForBatchedUpdates();
+
+ let report = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (res) => {
+ Onyx.disconnect(connectionID);
+ resolve(res);
+ },
+ });
+ });
+
+ // Then the report should be falsy (indicating deletion)
+ expect(report).toBeFalsy();
+
+ // Given the resumed fetch state
+ fetch.resume();
+
+ report = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (res) => {
+ Onyx.disconnect(connectionID);
+ resolve(res);
+ },
+ });
+ });
+
+ // Then the report should still be falsy (confirming deletion persisted)
+ expect(report).toBeFalsy();
+ });
+
+ it('does not delete the IOU report when there are visible comments left in the IOU report', async () => {
+ // Given the initial setup is completed
+ await waitForBatchedUpdates();
+
+ Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`,
+ callback: (val) => (reportActions = val),
+ });
+ await waitForBatchedUpdates();
+
+ jest.advanceTimersByTime(10);
+
+ // When a comment is added to the IOU report
+ Report.addComment(IOU_REPORT_ID, 'Testing a comment');
+ await waitForBatchedUpdates();
+
+ // Then verify that the comment is correctly added
+ const resultAction = _.find(reportActions, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT);
+ reportActionID = resultAction.reportActionID;
+
+ expect(resultAction.message).toEqual(REPORT_ACTION.message);
+ expect(resultAction.person).toEqual(REPORT_ACTION.person);
+ expect(resultAction.pendingAction).toBeNull();
+
+ await waitForBatchedUpdates();
+
+ // Verify there are three actions (created + iou + addcomment) and our optimistic comment has been removed
+ expect(_.size(reportActions)).toBe(3);
+
+ // Then check the loading state of our action
+ const resultActionAfterUpdate = reportActions[reportActionID];
+ expect(resultActionAfterUpdate.pendingAction).toBeNull();
+
+ // When we attempt to delete a money request from the IOU report
+ fetch.pause();
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false);
+ await waitForBatchedUpdates();
+
+ // Then expect that the IOU report still exists
+ let allReports = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (reports) => {
+ Onyx.disconnect(connectionID);
+ resolve(reports);
+ },
+ });
+ });
+
+ await waitForBatchedUpdates();
+
+ iouReport = _.find(allReports, (report) => ReportUtils.isIOUReport(report));
+ expect(iouReport).toBeTruthy();
+ expect(iouReport).toHaveProperty('reportID');
+ expect(iouReport).toHaveProperty('chatReportID');
+
+ // Given the resumed fetch state
+ fetch.resume();
+
+ allReports = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (reports) => {
+ Onyx.disconnect(connectionID);
+ resolve(reports);
+ },
+ });
+ });
+ // Then expect that the IOU report still exists
+ iouReport = _.find(allReports, (report) => ReportUtils.isIOUReport(report));
+ expect(iouReport).toBeTruthy();
+ expect(iouReport).toHaveProperty('reportID');
+ expect(iouReport).toHaveProperty('chatReportID');
+ });
+
+ it('delete the transaction thread if there are no visible comments in the thread', async () => {
+ // Given all promises are resolved
+ await waitForBatchedUpdates();
+ jest.advanceTimersByTime(10);
+
+ // Given a transaction thread
+ thread = ReportUtils.buildTransactionThread(createIOUAction, IOU_REPORT_ID);
+
+ Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`,
+ callback: (val) => (reportActions = val),
+ });
+
+ await waitForBatchedUpdates();
+
+ jest.advanceTimersByTime(10);
+
+ // Given User logins from the participant accounts
+ const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs);
+
+ // When Opening a thread report with the given details
+ Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID);
+ await waitForBatchedUpdates();
+
+ // Then The iou action has the transaction report id as a child report ID
+ const allReportActions = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
+ waitForCollectionCallback: true,
+ callback: (actions) => {
+ Onyx.disconnect(connectionID);
+ resolve(actions);
+ },
+ });
+ });
+ const reportActionsForIOUReport = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.iouReportID}`];
+ createIOUAction = _.find(reportActionsForIOUReport, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ expect(createIOUAction.childReportID).toBe(thread.reportID);
+
+ await waitForBatchedUpdates();
+
+ // Given Fetch is paused and timers have advanced
+ fetch.pause();
+ jest.advanceTimersByTime(10);
+
+ // When Deleting a money request
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false);
+ await waitForBatchedUpdates();
+
+ // Then The report for the given thread ID does not exist
+ let report = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (reportData) => {
+ Onyx.disconnect(connectionID);
+ resolve(reportData);
+ },
+ });
+ });
+
+ expect(report).toBeFalsy();
+ fetch.resume();
+
+ // Then After resuming fetch, the report for the given thread ID still does not exist
+ report = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (reportData) => {
+ Onyx.disconnect(connectionID);
+ resolve(reportData);
+ },
+ });
+ });
+
+ expect(report).toBeFalsy();
+ });
+
+ it('does not delete the transaction thread if there are visible comments in the thread', async () => {
+ // Given initial environment is set up
+ await waitForBatchedUpdates();
+
+ // Given a transaction thread
+ thread = ReportUtils.buildTransactionThread(createIOUAction);
+ const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs);
+ jest.advanceTimersByTime(10);
+ Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID);
+ await waitForBatchedUpdates();
+
+ Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`,
+ callback: (val) => (reportActions = val),
+ });
+ await waitForBatchedUpdates();
+
+ await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (report) => {
+ Onyx.disconnect(connectionID);
+ expect(report).toBeTruthy();
+ resolve();
+ },
+ });
+ });
+
+ jest.advanceTimersByTime(10);
+
+ // When a comment is added
+ Report.addComment(thread.reportID, 'Testing a comment');
+ await waitForBatchedUpdates();
+
+ // Then comment details should match the expected report action
+ const resultAction = _.find(reportActions, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT);
+ reportActionID = resultAction.reportActionID;
+ expect(resultAction.message).toEqual(REPORT_ACTION.message);
+ expect(resultAction.person).toEqual(REPORT_ACTION.person);
+
+ await waitForBatchedUpdates();
+
+ // Then the report should have 2 actions
+ expect(_.size(reportActions)).toBe(2);
+ const resultActionAfter = reportActions[reportActionID];
+ expect(resultActionAfter.pendingAction).toBeNull();
+
+ fetch.pause();
+ // When deleting money request
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false);
+ await waitForBatchedUpdates();
+
+ // Then the transaction thread report should still exist
+ await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (report) => {
+ Onyx.disconnect(connectionID);
+ expect(report).toBeTruthy();
+ resolve();
+ },
+ });
+ });
+
+ // When fetch resumes
+ // Then the transaction thread report should still exist
+ fetch.resume();
+ await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (report) => {
+ Onyx.disconnect(connectionID);
+ expect(report).toBeTruthy();
+ resolve();
+ },
+ });
+ });
+ });
+
+ it('update the moneyRequestPreview to show [Deleted request] when appropriate', async () => {
+ await waitForBatchedUpdates();
+
+ // Given a thread report
+
+ jest.advanceTimersByTime(10);
+ thread = ReportUtils.buildTransactionThread(createIOUAction, IOU_REPORT_ID);
+
+ Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`,
+ callback: (val) => (reportActions = val),
+ });
+ await waitForBatchedUpdates();
+
+ jest.advanceTimersByTime(10);
+ const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs);
+ Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID);
+
+ await waitForBatchedUpdates();
+
+ const allReportActions = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
+ waitForCollectionCallback: true,
+ callback: (actions) => {
+ Onyx.disconnect(connectionID);
+ resolve(actions);
+ },
+ });
+ });
+
+ const reportActionsForIOUReport = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.iouReportID}`];
+ createIOUAction = _.find(reportActionsForIOUReport, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ expect(createIOUAction.childReportID).toBe(thread.reportID);
+
+ await waitForBatchedUpdates();
+
+ // Given an added comment to the thread report
+
+ jest.advanceTimersByTime(10);
+
+ Report.addComment(thread.reportID, 'Testing a comment');
+ await waitForBatchedUpdates();
+
+ let resultAction = _.find(reportActions, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT);
+ reportActionID = resultAction.reportActionID;
+
+ expect(resultAction.message).toEqual(REPORT_ACTION.message);
+ expect(resultAction.person).toEqual(REPORT_ACTION.person);
+ expect(resultAction.pendingAction).toBeNull();
+
+ await waitForBatchedUpdates();
+
+ // Verify there are three actions (created + addcomment) and our optimistic comment has been removed
+ expect(_.size(reportActions)).toBe(2);
+
+ let resultActionAfterUpdate = reportActions[reportActionID];
+
+ // Verify that our action is no longer in the loading state
+ expect(resultActionAfterUpdate.pendingAction).toBeNull();
+
+ await waitForBatchedUpdates();
+
+ // Given an added comment to the IOU report
+
+ Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`,
+ callback: (val) => (reportActions = val),
+ });
+ await waitForBatchedUpdates();
+
+ jest.advanceTimersByTime(10);
+
+ Report.addComment(IOU_REPORT_ID, 'Testing a comment');
+ await waitForBatchedUpdates();
+
+ resultAction = _.find(reportActions, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT);
+ reportActionID = resultAction.reportActionID;
+
+ expect(resultAction.message).toEqual(REPORT_ACTION.message);
+ expect(resultAction.person).toEqual(REPORT_ACTION.person);
+ expect(resultAction.pendingAction).toBeNull();
+
+ await waitForBatchedUpdates();
+
+ // Verify there are three actions (created + iou + addcomment) and our optimistic comment has been removed
+ expect(_.size(reportActions)).toBe(3);
+
+ resultActionAfterUpdate = reportActions[reportActionID];
+
+ // Verify that our action is no longer in the loading state
+ expect(resultActionAfterUpdate.pendingAction).toBeNull();
+
+ fetch.pause();
+ // When we delete the money request
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false);
+ await waitForBatchedUpdates();
+
+ // Then we expect the moneyRequestPreview to show [Deleted request]
+
+ await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (reportActionsForReport) => {
+ Onyx.disconnect(connectionID);
+ createIOUAction = _.find(reportActionsForReport, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ expect(createIOUAction.message[0].isDeletedParentAction).toBeTruthy();
+ resolve();
+ },
+ });
+ });
+
+ // When we resume fetch
+
+ fetch.resume();
+
+ // Then we expect the moneyRequestPreview to show [Deleted request]
+
+ await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`,
+ waitForCollectionCallback: true,
+ callback: (reportActionsForReport) => {
+ Onyx.disconnect(connectionID);
+ createIOUAction = _.find(reportActionsForReport, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ expect(createIOUAction.message[0].isDeletedParentAction).toBeTruthy();
+ resolve();
+ },
+ });
+ });
+ });
+
+ it('update IOU report and reportPreview with new totals and messages if the IOU report is not deleted', async () => {
+ await waitForBatchedUpdates();
+ Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ callback: (val) => (iouReport = val),
+ });
+ await waitForBatchedUpdates();
+
+ // Given a second money request in addition to the first one
+
+ jest.advanceTimersByTime(10);
+ const amount2 = 20000;
+ const comment2 = 'Send me money please 2';
+ IOU.requestMoney(chatReport, amount2, CONST.CURRENCY.USD, '', '', TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID, {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, comment2);
+
+ await waitForBatchedUpdates();
+
+ // Then we expect the IOU report and reportPreview to update with new totals
+
+ expect(iouReport).toBeTruthy();
+ expect(iouReport).toHaveProperty('reportID');
+ expect(iouReport).toHaveProperty('chatReportID');
+ expect(iouReport.hasOutstandingIOU).toBeTruthy();
+ expect(iouReport.total).toBe(30000);
+
+ const ioupreview = ReportActionsUtils.getReportPreviewAction(chatReport.reportID, iouReport.reportID);
+ expect(ioupreview).toBeTruthy();
+ expect(ioupreview.message[0].text).toBe('rory@expensifail.com owes $300.00');
+
+ // When we delete the first money request
+ fetch.pause();
+ jest.advanceTimersByTime(10);
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false);
+ await waitForBatchedUpdates();
+
+ // Then we expect the IOU report and reportPreview to update with new totals
+
+ expect(iouReport).toBeTruthy();
+ expect(iouReport).toHaveProperty('reportID');
+ expect(iouReport).toHaveProperty('chatReportID');
+ expect(iouReport.hasOutstandingIOU).toBeTruthy();
+ expect(iouReport.total).toBe(20000);
+
+ // When we resume fetch
+ fetch.resume();
+
+ // Then we expect the IOU report and reportPreview to update with new totals
+ expect(iouReport).toBeTruthy();
+ expect(iouReport).toHaveProperty('reportID');
+ expect(iouReport).toHaveProperty('chatReportID');
+ expect(iouReport.hasOutstandingIOU).toBeTruthy();
+ expect(iouReport.total).toBe(20000);
+ });
+
+ it('navigate the user correctly to the iou Report when appropriate', async () => {
+ await waitForBatchedUpdates();
+
+ Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`,
+ callback: (val) => (reportActions = val),
+ });
+ await waitForBatchedUpdates();
+
+ // Given an added comment to the iou report
+
+ jest.advanceTimersByTime(10);
+
+ Report.addComment(IOU_REPORT_ID, 'Testing a comment');
+ await waitForBatchedUpdates();
+
+ const resultAction = _.find(reportActions, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT);
+ reportActionID = resultAction.reportActionID;
+
+ expect(resultAction.message).toEqual(REPORT_ACTION.message);
+ expect(resultAction.person).toEqual(REPORT_ACTION.person);
+
+ await waitForBatchedUpdates();
+
+ // Verify there are three actions (created + iou + addcomment) and our optimistic comment has been removed
+ expect(_.size(reportActions)).toBe(3);
+
+ await waitForBatchedUpdates();
+
+ // Given a thread report
+
+ jest.advanceTimersByTime(10);
+ thread = ReportUtils.buildTransactionThread(createIOUAction, IOU_REPORT_ID);
+
+ Onyx.connect({
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`,
+ callback: (val) => (reportActions = val),
+ });
+ await waitForBatchedUpdates();
+
+ jest.advanceTimersByTime(10);
+ const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs);
+ Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID);
+ await waitForBatchedUpdates();
+
+ const allReportActions = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
+ waitForCollectionCallback: true,
+ callback: (actions) => {
+ Onyx.disconnect(connectionID);
+ resolve(actions);
+ },
+ });
+ });
+
+ const reportActionsForIOUReport = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.iouReportID}`];
+ createIOUAction = _.find(reportActionsForIOUReport, (ra) => ra.actionName === CONST.REPORT.ACTIONS.TYPE.IOU);
+ expect(createIOUAction.childReportID).toBe(thread.reportID);
+
+ await waitForBatchedUpdates();
+
+ // When we delete the money request in SingleTransactionView and we should not delete the IOU report
+
+ fetch.pause();
+
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, true);
+ await waitForBatchedUpdates();
+
+ let allReports = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (reports) => {
+ Onyx.disconnect(connectionID);
+ resolve(reports);
+ },
+ });
+ });
+
+ await waitForBatchedUpdates();
+
+ iouReport = _.find(allReports, (report) => ReportUtils.isIOUReport(report));
+ expect(iouReport).toBeTruthy();
+ expect(iouReport).toHaveProperty('reportID');
+ expect(iouReport).toHaveProperty('chatReportID');
+
+ fetch.resume();
+
+ allReports = await new Promise((resolve) => {
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (reports) => {
+ Onyx.disconnect(connectionID);
+ resolve(reports);
+ },
+ });
+ });
+
+ iouReport = _.find(allReports, (report) => ReportUtils.isIOUReport(report));
+ expect(iouReport).toBeTruthy();
+ expect(iouReport).toHaveProperty('reportID');
+ expect(iouReport).toHaveProperty('chatReportID');
+
+ // Then we expect to navigate to the iou report
+
+ expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.getReportRoute(IOU_REPORT_ID));
+ });
+
+ it('navigate the user correctly to the chat Report when appropriate', () => {
+ // When we delete the money request and we should delete the IOU report
+ IOU.deleteMoneyRequest(transaction.transactionID, createIOUAction, false);
+ // Then we expect to navigate to the chat report
+ expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.getReportRoute(chatReport.reportID));
+ });
+ });
});
diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js
index 6381ef0a91ac..6bc8b1b01528 100644
--- a/tests/unit/OptionsListUtilsTest.js
+++ b/tests/unit/OptionsListUtilsTest.js
@@ -320,12 +320,12 @@ describe('OptionsListUtils', () => {
});
});
- it('getNewChatOptions()', () => {
+ it('getFilteredOptions()', () => {
// maxRecentReportsToShow in src/libs/OptionsListUtils.js
const MAX_RECENT_REPORTS = 5;
- // When we call getNewChatOptions() with no search value
- let results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '');
+ // When we call getFilteredOptions() with no search value
+ let results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '');
// We should expect maximimum of 5 recent reports to be returned
expect(results.recentReports.length).toBe(MAX_RECENT_REPORTS);
@@ -345,7 +345,7 @@ describe('OptionsListUtils', () => {
expect(personalDetailWithExistingReport.reportID).toBe(2);
// When we only pass personal details
- results = OptionsListUtils.getNewChatOptions([], PERSONAL_DETAILS, [], '');
+ results = OptionsListUtils.getFilteredOptions([], PERSONAL_DETAILS, [], '');
// We should expect personal details sorted alphabetically
expect(results.personalDetails[0].text).toBe('Black Panther');
@@ -354,13 +354,13 @@ describe('OptionsListUtils', () => {
expect(results.personalDetails[3].text).toBe('Invisible Woman');
// When we provide a search value that does not match any personal details
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'magneto');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'magneto');
// Then no options will be returned
expect(results.personalDetails.length).toBe(0);
// When we provide a search value that matches an email
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'peterparker@expensify.com');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'peterparker@expensify.com');
// Then one recentReports will be returned and it will be the correct option
// personalDetails should be empty array
@@ -369,7 +369,7 @@ describe('OptionsListUtils', () => {
expect(results.personalDetails.length).toBe(0);
// When we provide a search value that matches a partial display name or email
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '.com');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '.com');
// Then several options will be returned and they will be each have the search string in their email or name
// even though the currently logged in user matches they should not show.
@@ -382,7 +382,7 @@ describe('OptionsListUtils', () => {
expect(results.recentReports[2].text).toBe('Black Panther');
// Test for Concierge's existence in chat options
- results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE);
+ results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE);
// Concierge is included in the results by default. We should expect all the personalDetails to show
// (minus the 5 that are already showing and the currently logged in user)
@@ -390,30 +390,30 @@ describe('OptionsListUtils', () => {
expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})]));
// Test by excluding Concierge from the results
- results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]);
+ results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]);
// All the personalDetails should be returned minus the currently logged in user and Concierge
expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2 - MAX_RECENT_REPORTS);
expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})]));
// Test by excluding Chronos from the results
- results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]);
+ results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]);
// All the personalDetails should be returned minus the currently logged in user and Concierge
expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 2 - MAX_RECENT_REPORTS);
expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})]));
// Test by excluding Receipts from the results
- results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]);
+ results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]);
// All the personalDetails should be returned minus the currently logged in user and Concierge
expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 2 - MAX_RECENT_REPORTS);
expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})]));
});
- it('getNewChatOptions() for group Chat', () => {
- // When we call getNewChatOptions() with no search value
- let results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '');
+ it('getFilteredOptions() for group Chat', () => {
+ // When we call getFilteredOptions() with no search value
+ let results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '');
// Then we should expect only a maxmimum of 5 recent reports to be returned
expect(results.recentReports.length).toBe(5);
@@ -434,7 +434,7 @@ describe('OptionsListUtils', () => {
expect(personalDetailsOverlapWithReports).toBe(false);
// When we search for an option that is only in a personalDetail with no existing report
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'hulk');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'hulk');
// Then reports should return no results
expect(results.recentReports.length).toBe(0);
@@ -444,7 +444,7 @@ describe('OptionsListUtils', () => {
expect(results.personalDetails[0].login).toBe('brucebanner@expensify.com');
// When we search for an option that matches things in both personalDetails and reports
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '.com');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '.com');
// Then all single participant reports that match will show up in the recentReports array, Recently used contact should be at the top
expect(results.recentReports.length).toBe(5);
@@ -454,16 +454,16 @@ describe('OptionsListUtils', () => {
expect(results.personalDetails.length).toBe(4);
expect(results.personalDetails[0].login).toBe('natasharomanoff@expensify.com');
- // When we provide no selected options to getNewChatOptions()
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '', []);
+ // When we provide no selected options to getFilteredOptions()
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', []);
// Then one of our older report options (not in our five most recent) should appear in the personalDetails
// but not in recentReports
expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true);
expect(_.every(results.personalDetails, (option) => option.login !== 'peterparker@expensify.com')).toBe(false);
- // When we provide a "selected" option to getNewChatOptions()
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '', [{login: 'peterparker@expensify.com'}]);
+ // When we provide a "selected" option to getFilteredOptions()
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', [{login: 'peterparker@expensify.com'}]);
// Then the option should not appear anywhere in either list
expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true);
@@ -471,7 +471,7 @@ describe('OptionsListUtils', () => {
// When we add a search term for which no options exist and the searchValue itself
// is not a potential email or phone
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify');
// Then we should have no options or personal details at all and also that there is no userToInvite
expect(results.recentReports.length).toBe(0);
@@ -480,7 +480,7 @@ describe('OptionsListUtils', () => {
// When we add a search term for which no options exist and the searchValue itself
// is a potential email
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify.com');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify.com');
// Then we should have no options or personal details at all but there should be a userToInvite
expect(results.recentReports.length).toBe(0);
@@ -488,7 +488,7 @@ describe('OptionsListUtils', () => {
expect(results.userToInvite).not.toBe(null);
// When we add a search term with a period, with options for it that don't contain the period
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'peter.parker@expensify.com');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'peter.parker@expensify.com');
// Then we should have no options at all but there should be a userToInvite
expect(results.recentReports.length).toBe(0);
@@ -496,7 +496,7 @@ describe('OptionsListUtils', () => {
// When we add a search term for which no options exist and the searchValue itself
// is a potential phone number without country code added
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '5005550006');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '5005550006');
// Then we should have no options or personal details at all but there should be a userToInvite and the login
// should have the country code included
@@ -507,7 +507,7 @@ describe('OptionsListUtils', () => {
// When we add a search term for which no options exist and the searchValue itself
// is a potential phone number with country code added
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '+15005550006');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '+15005550006');
// Then we should have no options or personal details at all but there should be a userToInvite and the login
// should have the country code included
@@ -518,7 +518,7 @@ describe('OptionsListUtils', () => {
// When we add a search term for which no options exist and the searchValue itself
// is a potential phone number with special characters added
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '+1 (800)324-3233');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '+1 (800)324-3233');
// Then we should have no options or personal details at all but there should be a userToInvite and the login
// should have the country code included
@@ -528,7 +528,7 @@ describe('OptionsListUtils', () => {
expect(results.userToInvite.login).toBe('+18003243233');
// When we use a search term for contact number that contains alphabet characters
- results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa');
+ results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa');
// Then we shouldn't have any results or user to invite
expect(results.recentReports.length).toBe(0);
@@ -536,7 +536,7 @@ describe('OptionsListUtils', () => {
expect(results.userToInvite).toBe(null);
// Test Concierge's existence in new group options
- results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE);
+ results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE);
// Concierge is included in the results by default. We should expect all the personalDetails to show
// (minus the 5 that are already showing and the currently logged in user)
@@ -544,7 +544,7 @@ describe('OptionsListUtils', () => {
expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})]));
// Test by excluding Concierge from the results
- results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]);
+ results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]);
// We should expect all the personalDetails to show (minus the 5 that are already showing,
// the currently logged in user and Concierge)
@@ -553,7 +553,7 @@ describe('OptionsListUtils', () => {
expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})]));
// Test by excluding Chronos from the results
- results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]);
+ results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]);
// We should expect all the personalDetails to show (minus the 5 that are already showing,
// the currently logged in user and Concierge)
@@ -562,7 +562,7 @@ describe('OptionsListUtils', () => {
expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})]));
// Test by excluding Receipts from the results
- results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]);
+ results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]);
// We should expect all the personalDetails to show (minus the 5 that are already showing,
// the currently logged in user and Concierge)
@@ -652,7 +652,7 @@ describe('OptionsListUtils', () => {
expect(results.personalDetails[0].text).toBe('Spider-Man');
});
- it('getNewChatOptions() for categories', () => {
+ it('getFilteredOptions() for categories', () => {
const search = 'Food';
const emptySearch = '';
const wrongSearch = 'bla bla';
@@ -970,16 +970,16 @@ describe('OptionsListUtils', () => {
},
];
- const smallResult = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, true, smallCategoriesList);
+ const smallResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, true, smallCategoriesList);
expect(smallResult.categoryOptions).toStrictEqual(smallResultList);
- const smallSearchResult = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, true, smallCategoriesList);
+ const smallSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, true, smallCategoriesList);
expect(smallSearchResult.categoryOptions).toStrictEqual(smallSearchResultList);
- const smallWrongSearchResult = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, true, smallCategoriesList);
+ const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, true, smallCategoriesList);
expect(smallWrongSearchResult.categoryOptions).toStrictEqual(smallWrongSearchResultList);
- const largeResult = OptionsListUtils.getNewChatOptions(
+ const largeResult = OptionsListUtils.getFilteredOptions(
REPORTS,
PERSONAL_DETAILS,
[],
@@ -994,7 +994,7 @@ describe('OptionsListUtils', () => {
);
expect(largeResult.categoryOptions).toStrictEqual(largeResultList);
- const largeSearchResult = OptionsListUtils.getNewChatOptions(
+ const largeSearchResult = OptionsListUtils.getFilteredOptions(
REPORTS,
PERSONAL_DETAILS,
[],
@@ -1009,7 +1009,7 @@ describe('OptionsListUtils', () => {
);
expect(largeSearchResult.categoryOptions).toStrictEqual(largeSearchResultList);
- const largeWrongSearchResult = OptionsListUtils.getNewChatOptions(
+ const largeWrongSearchResult = OptionsListUtils.getFilteredOptions(
REPORTS,
PERSONAL_DETAILS,
[],
@@ -1028,6 +1028,317 @@ describe('OptionsListUtils', () => {
expect(emptyResult.categoryOptions).toStrictEqual(emptySelectedResultList);
});
+ it('getFilteredOptions() for tags', () => {
+ const search = 'ing';
+ const emptySearch = '';
+ const wrongSearch = 'bla bla';
+ const recentlyUsedTags = ['Engineering', 'HR'];
+
+ const selectedOptions = [
+ {
+ name: 'Medical',
+ },
+ ];
+ const smallTagsList = {
+ Engineering: {
+ enabled: false,
+ name: 'Engineering',
+ },
+ Medical: {
+ enabled: true,
+ name: 'Medical',
+ },
+ Accounting: {
+ enabled: true,
+ name: 'Accounting',
+ },
+ HR: {
+ enabled: true,
+ name: 'HR',
+ },
+ };
+ const smallResultList = [
+ {
+ title: '',
+ shouldShow: false,
+ indexOffset: 0,
+ data: [
+ {
+ text: 'Medical',
+ keyForList: 'Medical',
+ searchText: 'Medical',
+ tooltipText: 'Medical',
+ isDisabled: false,
+ },
+ {
+ text: 'Accounting',
+ keyForList: 'Accounting',
+ searchText: 'Accounting',
+ tooltipText: 'Accounting',
+ isDisabled: false,
+ },
+ {
+ text: 'HR',
+ keyForList: 'HR',
+ searchText: 'HR',
+ tooltipText: 'HR',
+ isDisabled: false,
+ },
+ ],
+ },
+ ];
+ const smallSearchResultList = [
+ {
+ title: '',
+ shouldShow: false,
+ indexOffset: 0,
+ data: [
+ {
+ text: 'Accounting',
+ keyForList: 'Accounting',
+ searchText: 'Accounting',
+ tooltipText: 'Accounting',
+ isDisabled: false,
+ },
+ ],
+ },
+ ];
+ const smallWrongSearchResultList = [
+ {
+ title: '',
+ shouldShow: false,
+ indexOffset: 0,
+ data: [],
+ },
+ ];
+ const largeTagsList = {
+ Engineering: {
+ enabled: false,
+ name: 'Engineering',
+ },
+ Medical: {
+ enabled: true,
+ name: 'Medical',
+ },
+ Accounting: {
+ enabled: true,
+ name: 'Accounting',
+ },
+ HR: {
+ enabled: true,
+ name: 'HR',
+ },
+ Food: {
+ enabled: true,
+ name: 'Food',
+ },
+ Traveling: {
+ enabled: false,
+ name: 'Traveling',
+ },
+ Cleaning: {
+ enabled: true,
+ name: 'Cleaning',
+ },
+ Software: {
+ enabled: true,
+ name: 'Software',
+ },
+ OfficeSupplies: {
+ enabled: false,
+ name: 'Office Supplies',
+ },
+ Taxes: {
+ enabled: true,
+ name: 'Taxes',
+ },
+ Benefits: {
+ enabled: true,
+ name: 'Benefits',
+ },
+ };
+ const largeResultList = [
+ {
+ title: '',
+ shouldShow: false,
+ indexOffset: 0,
+ data: [
+ {
+ text: 'Medical',
+ keyForList: 'Medical',
+ searchText: 'Medical',
+ tooltipText: 'Medical',
+ isDisabled: false,
+ },
+ ],
+ },
+ {
+ title: 'Recent',
+ shouldShow: true,
+ indexOffset: 1,
+ data: [
+ {
+ text: 'HR',
+ keyForList: 'HR',
+ searchText: 'HR',
+ tooltipText: 'HR',
+ isDisabled: false,
+ },
+ ],
+ },
+ {
+ title: 'All',
+ shouldShow: true,
+ indexOffset: 2,
+ data: [
+ {
+ text: 'Accounting',
+ keyForList: 'Accounting',
+ searchText: 'Accounting',
+ tooltipText: 'Accounting',
+ isDisabled: false,
+ },
+ {
+ text: 'HR',
+ keyForList: 'HR',
+ searchText: 'HR',
+ tooltipText: 'HR',
+ isDisabled: false,
+ },
+ {
+ text: 'Food',
+ keyForList: 'Food',
+ searchText: 'Food',
+ tooltipText: 'Food',
+ isDisabled: false,
+ },
+ {
+ text: 'Cleaning',
+ keyForList: 'Cleaning',
+ searchText: 'Cleaning',
+ tooltipText: 'Cleaning',
+ isDisabled: false,
+ },
+ {
+ text: 'Software',
+ keyForList: 'Software',
+ searchText: 'Software',
+ tooltipText: 'Software',
+ isDisabled: false,
+ },
+ {
+ text: 'Taxes',
+ keyForList: 'Taxes',
+ searchText: 'Taxes',
+ tooltipText: 'Taxes',
+ isDisabled: false,
+ },
+ {
+ text: 'Benefits',
+ keyForList: 'Benefits',
+ searchText: 'Benefits',
+ tooltipText: 'Benefits',
+ isDisabled: false,
+ },
+ ],
+ },
+ ];
+ const largeSearchResultList = [
+ {
+ title: '',
+ shouldShow: false,
+ indexOffset: 0,
+ data: [
+ {
+ text: 'Accounting',
+ keyForList: 'Accounting',
+ searchText: 'Accounting',
+ tooltipText: 'Accounting',
+ isDisabled: false,
+ },
+ {
+ text: 'Cleaning',
+ keyForList: 'Cleaning',
+ searchText: 'Cleaning',
+ tooltipText: 'Cleaning',
+ isDisabled: false,
+ },
+ ],
+ },
+ ];
+ const largeWrongSearchResultList = [
+ {
+ title: '',
+ shouldShow: false,
+ indexOffset: 0,
+ data: [],
+ },
+ ];
+
+ const smallResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, false, {}, [], true, smallTagsList);
+ expect(smallResult.tagOptions).toStrictEqual(smallResultList);
+
+ const smallSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, false, {}, [], true, smallTagsList);
+ expect(smallSearchResult.tagOptions).toStrictEqual(smallSearchResultList);
+
+ const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, false, {}, [], true, smallTagsList);
+ expect(smallWrongSearchResult.tagOptions).toStrictEqual(smallWrongSearchResultList);
+
+ const largeResult = OptionsListUtils.getFilteredOptions(
+ REPORTS,
+ PERSONAL_DETAILS,
+ [],
+ emptySearch,
+ selectedOptions,
+ [],
+ false,
+ false,
+ false,
+ {},
+ [],
+ true,
+ largeTagsList,
+ recentlyUsedTags,
+ );
+ expect(largeResult.tagOptions).toStrictEqual(largeResultList);
+
+ const largeSearchResult = OptionsListUtils.getFilteredOptions(
+ REPORTS,
+ PERSONAL_DETAILS,
+ [],
+ search,
+ selectedOptions,
+ [],
+ false,
+ false,
+ false,
+ {},
+ [],
+ true,
+ largeTagsList,
+ recentlyUsedTags,
+ );
+ expect(largeSearchResult.tagOptions).toStrictEqual(largeSearchResultList);
+
+ const largeWrongSearchResult = OptionsListUtils.getFilteredOptions(
+ REPORTS,
+ PERSONAL_DETAILS,
+ [],
+ wrongSearch,
+ selectedOptions,
+ [],
+ false,
+ false,
+ false,
+ {},
+ [],
+ true,
+ largeTagsList,
+ recentlyUsedTags,
+ );
+ expect(largeWrongSearchResult.tagOptions).toStrictEqual(largeWrongSearchResultList);
+ });
+
it('getCategoryOptionTree()', () => {
const categories = {
Taxi: {
diff --git a/tsconfig.json b/tsconfig.json
index d138ce81e36c..0c88512b9749 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -32,6 +32,6 @@
"skipLibCheck": true,
"incremental": true
},
- "exclude": ["**/node_modules/*", "**/dist/*", ".github/actions/**/index.js"],
+ "exclude": ["**/node_modules/*", "**/dist/*", ".github/actions/**/index.js", "**/docs/*"],
"include": ["src", "desktop", "web", "docs", "assets", "config", "tests", "jest", "__mocks__", ".github/**/*", ".storybook/**/*"]
}