diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml
index ca7345ef9462..4a53e75354c6 100644
--- a/.github/workflows/deployExpensifyHelp.yml
+++ b/.github/workflows/deployExpensifyHelp.yml
@@ -1,20 +1,23 @@
-# Deploying the ExpensifyHelp Jekyll site by dynamically generating routes file
name: Deploy ExpensifyHelp
on:
- # Runs on pushes targeting the default branch
+ # Run on any push to main that has changes to the docs directory
push:
- branches: ["main"]
-
- # Allows you to run this workflow manually from the Actions tab
+ branches:
+ - main
+ paths:
+ - 'docs/**'
+
+ # Run on any pull request (except PRs against staging or production) that has changes to the docs directory
+ pull_request:
+ types: [opened, synchronize]
+ branches-ignore: [staging, production]
+ paths:
+ - 'docs/**'
+
+ # Run on any manual trigger
workflow_dispatch:
-# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
-permissions:
- contents: read
- pages: write
- id-token: write
-
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
@@ -22,7 +25,6 @@ concurrency:
cancel-in-progress: false
jobs:
- # Build job
build:
runs-on: ubuntu-latest
steps:
@@ -32,9 +34,6 @@ jobs:
- name: Setup NodeJS
uses: Expensify/App/.github/actions/composite/setupNode@main
- - name: Setup Pages
- uses: actions/configure-pages@f156874f8191504dae5b037505266ed5dda6c382
-
- name: Create docs routes file
run: ./.github/scripts/createDocsRoutes.sh
@@ -44,19 +43,18 @@ jobs:
source: ./docs/
destination: ./docs/_site
- - name: Upload artifact
- uses: actions/upload-pages-artifact@64bcae551a7b18bcb9a09042ddf1960979799187
+ - name: Deploy to Cloudflare Pages
+ uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca
+ id: deploy
with:
- path: ./docs/_site
-
- # Deployment job
- deploy:
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
- needs: build
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@af48cf94a42f2c634308b1c9dc0151830b6f190a
+ apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+ projectName: helpdot
+ directory: ./docs/_site
+
+ - name: Leave a comment on the PR
+ uses: actions-cool/maintain-one-comment@de04bd2a3750d86b324829a3ff34d47e48e16f4b
+ if: ${{ github.event_name == 'pull_request' }}
+ with:
+ token: ${{ secrets.OS_BOTIFY_TOKEN }}
+ body: ${{ format('A preview of your ExpensifyHelp changes have been deployed to {0} ⚡️', steps.deploy.outputs.alias) }}
diff --git a/android/app/build.gradle b/android/app/build.gradle
index fa2bd3865ca2..2a3499e6d005 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 1001038708
- versionName "1.3.87-8"
+ versionCode 1001038803
+ versionName "1.3.88-3"
}
flavorDimensions "default"
diff --git a/docs/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time.md b/docs/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time.md
index 3ee1c8656b4b..5bbd2c4b583c 100644
--- a/docs/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time.md
+++ b/docs/articles/expensify-classic/integrations/HR-integrations/QuickBooks-Time.md
@@ -1,5 +1,41 @@
---
-title: Coming Soon
-description: Coming Soon
+title: Expensify and TSheets/QuickBooks Time Integration Guide
+description: This help document explains how to connect TSheets/QuickBooks Time to your Expensify policy
---
-## Resource Coming Soon!
+# Overview
+
+Connecting Expensify with TSheets/QuickBooks Time can streamline your expense tracking and time management processes. This integration allows you to automatically sync time entries from TSheets/QuickBooks Time with expenses in Expensify, ensuring accurate and efficient expense reporting.
+
+# How to set up the Expensify and TSheets/QuickBooks Time integration
+
+Before you begin, make sure you have the following:
+
+- **Expensify account:** You must have an active Expensify account.
+- **TSheets account:** You must have a TSheets account and admin privileges to set up the integration.
+
+Now, follow these steps to set up the integration:
+
+1. Log into your Expensify account on your web browser
+
+2. Go to **Settings > Workspaces > Group > Workspace Name > Connections > TSheets**
+
+3. Click **Connect to TSheets**
+
+4. Follow the on-screen instructions to sign in to your TSheets account and grant Expensify access.
+
+5. Once the integration is authorized, you may need to configure some preferences.
+- Specify how you want TSheets time entries to be imported into Expensify. You can typically customize settings like the date range, project/task mapping, and expense categories.
+
+6. Now, we’d recommend testing the integration.
+- Create a sample time entry in TSheets and check if it’s automatically reflected in your Expensify account.
+
+7. If the test is successful, save your integration settings.
+
+8. You may also want to schedule regular syncs or specify how often Expensify should pull data from TSheets.
+
+With the integration set up, your TSheets time entries will now appear in Expensify as expenses. You can review, categorize, and submit these expenses as needed.
+
+Congratulations! You've successfully integrated Expensify with TSheets, simplifying your expense tracking and reporting process.
+
+For questions, don't hesitate to reach out to concierge@expensify.com or chat directly with your account manager
+
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Hotel-Tonight.md b/docs/articles/expensify-classic/integrations/travel-integrations/Hotel-Tonight.md
deleted file mode 100644
index 3ee1c8656b4b..000000000000
--- a/docs/articles/expensify-classic/integrations/travel-integrations/Hotel-Tonight.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Coming Soon
-description: Coming Soon
----
-## Resource Coming Soon!
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Kayak.md b/docs/articles/expensify-classic/integrations/travel-integrations/Kayak.md
deleted file mode 100644
index 3ee1c8656b4b..000000000000
--- a/docs/articles/expensify-classic/integrations/travel-integrations/Kayak.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Coming Soon
-description: Coming Soon
----
-## Resource Coming Soon!
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 95a9a26df7f6..2e9862727e25 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.3.87
+ 1.3.88
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.87.8
+ 1.3.88.3
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index d41b75440036..1961c8157c4f 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.3.87
+ 1.3.88
CFBundleSignature
????
CFBundleVersion
- 1.3.87.8
+ 1.3.88.3
diff --git a/package-lock.json b/package-lock.json
index 65c2be0529e3..39220723706d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.87-8",
+ "version": "1.3.88-3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.87-8",
+ "version": "1.3.88-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index aca3dc508c41..2ba8a358b947 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.87-8",
+ "version": "1.3.88-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.ts b/src/CONST.ts
index 048c2dee5bab..aa09c5772f11 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -124,7 +124,16 @@ const CONST = {
VIEW_HEIGHT: 275,
},
MONEY_REPORT: {
- MIN_HEIGHT: 280,
+ SMALL_SCREEN: {
+ IMAGE_HEIGHT: 300,
+ CONTAINER_MINHEIGHT: 280,
+ VIEW_HEIGHT: 220,
+ },
+ WIDE_SCREEN: {
+ IMAGE_HEIGHT: 450,
+ CONTAINER_MINHEIGHT: 280,
+ VIEW_HEIGHT: 275,
+ },
},
},
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index b5ceb8fc557d..a2308cb56ef1 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -5,6 +5,16 @@ import CONST from './CONST';
* This is a file containing constants for all of the routes we want to be able to go to
*/
+/**
+ * This is a file containing constants for all of the routes we want to be able to go to
+ * Returns an encoded URI component for the backTo param which can be added to the end of URLs
+ * @param backTo
+ * @returns
+ */
+function getBackToParam(backTo?: string): string {
+ return backTo ? `?backTo=${encodeURIComponent(backTo)}` : '';
+}
+
export default {
HOME: '',
/** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */
@@ -20,10 +30,7 @@ export default {
},
PROFILE: {
route: 'a/:accountID',
- getRoute: (accountID: string | number, backTo = '') => {
- const backToParam = backTo ? `?backTo=${encodeURIComponent(backTo)}` : '';
- return `a/${accountID}${backToParam}`;
- },
+ getRoute: (accountID: string | number, backTo?: string) => `a/${accountID}${getBackToParam(backTo)}`,
},
TRANSITION_BETWEEN_APPS: 'transition',
@@ -49,10 +56,7 @@ export default {
BANK_ACCOUNT_PERSONAL: 'bank-account/personal',
BANK_ACCOUNT_WITH_STEP_TO_OPEN: {
route: 'bank-account/:stepToOpen?',
- getRoute: (stepToOpen = '', policyID = '', backTo = ''): string => {
- const backToParam = backTo ? `&backTo=${encodeURIComponent(backTo)}` : '';
- return `bank-account/${stepToOpen}?policyID=${policyID}${backToParam}`;
- },
+ getRoute: (stepToOpen = '', policyID = '', backTo?: string): string => `bank-account/${stepToOpen}?policyID=${policyID}${getBackToParam(backTo)}`,
},
SETTINGS: 'settings',
@@ -104,13 +108,7 @@ export default {
SETTINGS_PERSONAL_DETAILS_ADDRESS: 'settings/profile/personal-details/address',
SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY: {
route: 'settings/profile/personal-details/address/country',
- getRoute: (country: string, backTo?: string) => {
- let route = `settings/profile/personal-details/address/country?country=${country}`;
- if (backTo) {
- route += `&backTo=${encodeURIComponent(backTo)}`;
- }
- return route;
- },
+ getRoute: (country: string, backTo?: string) => `settings/profile/personal-details/address/country?country=${country}${getBackToParam(backTo)}`,
},
SETTINGS_CONTACT_METHODS: 'settings/profile/contact-methods',
SETTINGS_CONTACT_METHOD_DETAILS: {
diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js
index a12b089ddf97..8b32234fdbdf 100644
--- a/src/components/EmojiPicker/EmojiPicker.js
+++ b/src/components/EmojiPicker/EmojiPicker.js
@@ -1,12 +1,13 @@
import React, {useState, useEffect, useRef, forwardRef, useImperativeHandle} from 'react';
import {Dimensions} from 'react-native';
import _ from 'underscore';
+import PropTypes from 'prop-types';
import EmojiPickerMenu from './EmojiPickerMenu';
import CONST from '../../CONST';
import styles from '../../styles/styles';
import PopoverWithMeasuredContent from '../PopoverWithMeasuredContent';
import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
-import withViewportOffsetTop, {viewportOffsetTopPropTypes} from '../withViewportOffsetTop';
+import withViewportOffsetTop from '../withViewportOffsetTop';
import compose from '../../libs/compose';
import * as StyleUtils from '../../styles/StyleUtils';
import calculateAnchorPosition from '../../libs/calculateAnchorPosition';
@@ -18,7 +19,7 @@ const DEFAULT_ANCHOR_ORIGIN = {
const propTypes = {
...windowDimensionsPropTypes,
- ...viewportOffsetTopPropTypes,
+ viewportOffsetTop: PropTypes.number.isRequired,
};
const EmojiPicker = forwardRef((props, ref) => {
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js
index 0d7826ff3783..df47d3494928 100755
--- a/src/components/EmojiPicker/EmojiPickerMenu/index.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js
@@ -108,7 +108,6 @@ function EmojiPickerMenu(props) {
const [selection, setSelection] = useState({start: 0, end: 0});
const [isFocused, setIsFocused] = useState(false);
const [isUsingKeyboardMovement, setIsUsingKeyboardMovement] = useState(false);
- const [selectTextOnFocus, setSelectTextOnFocus] = useState(false);
useEffect(() => {
const emojisAndHeaderRowIndices = getEmojisAndHeaderRowIndices();
@@ -162,8 +161,6 @@ function EmojiPickerMenu(props) {
if (!searchInputRef.current) {
return;
}
-
- setSelectTextOnFocus(true);
searchInputRef.current.focus();
}
@@ -323,11 +320,7 @@ function EmojiPickerMenu(props) {
// We allow typing in the search box if any key is pressed apart from Arrow keys.
if (searchInputRef.current && !searchInputRef.current.isFocused()) {
- setSelectTextOnFocus(false);
searchInputRef.current.focus();
-
- // Re-enable selection on the searchInput
- setSelectTextOnFocus(true);
}
},
[filteredEmojis, highlightAdjacentEmoji, highlightedIndex, isFocused, onEmojiSelected, preferredSkinTone],
@@ -371,13 +364,17 @@ function EmojiPickerMenu(props) {
}
setupEventHandlers();
- updateFirstNonHeaderIndex(emojis.current);
return () => {
cleanupEventHandlers();
};
}, [forwardedRef, shouldFocusInputOnScreenFocus, cleanupEventHandlers, setupEventHandlers]);
+ useEffect(() => {
+ // Find and store index of the first emoji item on mount
+ updateFirstNonHeaderIndex(emojis.current);
+ }, []);
+
const scrollToHeader = useCallback((headerIndex) => {
if (!emojiListRef.current) {
return;
@@ -481,7 +478,6 @@ function EmojiPickerMenu(props) {
defaultValue=""
ref={searchInputRef}
autoFocus={shouldFocusInputOnScreenFocus}
- selectTextOnFocus={selectTextOnFocus}
onSelectionChange={onSelectionChange}
onFocus={() => {
setHighlightedIndex(-1);
diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js
index 8ae4672e758e..ab0b77c21653 100644
--- a/src/components/MoneyReportHeader.js
+++ b/src/components/MoneyReportHeader.js
@@ -121,10 +121,6 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
shouldShowPaymentOptions
style={[styles.pv2]}
formattedAmount={formattedAmount}
- anchorAlignment={{
- horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
- vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
- }}
/>
)}
diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js
index 5ca08bf82f89..a0923d1214ec 100755
--- a/src/components/MoneyRequestConfirmationList.js
+++ b/src/components/MoneyRequestConfirmationList.js
@@ -506,7 +506,11 @@ function MoneyRequestConfirmationList(props) {
policyID={props.policyID}
shouldShowPaymentOptions
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE}
- anchorAlignment={{
+ kycWallAnchorAlignment={{
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ }}
+ paymentMethodDropdownAnchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
}}
diff --git a/src/components/PDFView/PDFPasswordForm.js b/src/components/PDFView/PDFPasswordForm.js
index 58a4e64a28a5..6b6163992589 100644
--- a/src/components/PDFView/PDFPasswordForm.js
+++ b/src/components/PDFView/PDFPasswordForm.js
@@ -131,7 +131,7 @@ function PDFPasswordForm({isFocused, isPasswordInvalid, shouldShowLoadingIndicat
autoCorrect={false}
textContentType="password"
onChangeText={updatePassword}
- returnKeyType="done"
+ returnKeyType="go"
onSubmitEditing={submitPassword}
errorText={errorText}
onFocus={() => onPasswordFieldFocused(true)}
diff --git a/src/components/QRCode/index.js b/src/components/QRCode/index.js
deleted file mode 100644
index f27cf28066ef..000000000000
--- a/src/components/QRCode/index.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import React from 'react';
-import QRCodeLibrary from 'react-native-qrcode-svg';
-import PropTypes from 'prop-types';
-import defaultTheme from '../../styles/themes/default';
-import CONST from '../../CONST';
-
-const propTypes = {
- /**
- * The QR code URL
- */
- url: PropTypes.string.isRequired,
- /**
- * The logo which will be displayed in the middle of the QR code.
- * Follows ImageProps href from react-native-svg that is used by react-native-qrcode-svg.
- */
- logo: PropTypes.oneOfType([PropTypes.shape({uri: PropTypes.string}), PropTypes.number, PropTypes.string]),
- /**
- * The size ratio of logo to QR code
- */
- logoRatio: PropTypes.number,
- /**
- * The size ratio of margin around logo to QR code
- */
- logoMarginRatio: PropTypes.number,
- /**
- * The QRCode size
- */
- size: PropTypes.number,
- /**
- * The QRCode color
- */
- color: PropTypes.string,
- /**
- * The QRCode background color
- */
- backgroundColor: PropTypes.string,
- /**
- * Function to retrieve the internal component ref and be able to call it's
- * methods
- */
- getRef: PropTypes.func,
-};
-
-const defaultProps = {
- logo: undefined,
- size: 120,
- color: defaultTheme.text,
- backgroundColor: defaultTheme.highlightBG,
- getRef: undefined,
- logoRatio: CONST.QR.DEFAULT_LOGO_SIZE_RATIO,
- logoMarginRatio: CONST.QR.DEFAULT_LOGO_MARGIN_RATIO,
-};
-
-function QRCode(props) {
- return (
-
- );
-}
-
-QRCode.displayName = 'QRCode';
-QRCode.propTypes = propTypes;
-QRCode.defaultProps = defaultProps;
-
-export default QRCode;
diff --git a/src/components/QRCode/index.tsx b/src/components/QRCode/index.tsx
new file mode 100644
index 000000000000..bca45c02fffa
--- /dev/null
+++ b/src/components/QRCode/index.tsx
@@ -0,0 +1,71 @@
+import React, {Ref} from 'react';
+import QRCodeLibrary from 'react-native-qrcode-svg';
+import {ImageSourcePropType} from 'react-native';
+import defaultTheme from '../../styles/themes/default';
+import CONST from '../../CONST';
+
+type LogoRatio = typeof CONST.QR.DEFAULT_LOGO_SIZE_RATIO | typeof CONST.QR.EXPENSIFY_LOGO_SIZE_RATIO;
+
+type LogoMarginRatio = typeof CONST.QR.DEFAULT_LOGO_MARGIN_RATIO | typeof CONST.QR.EXPENSIFY_LOGO_MARGIN_RATIO;
+
+type QRCodeProps = {
+ /** The QR code URL */
+ url: string;
+
+ /**
+ * The logo which will be displayed in the middle of the QR code.
+ * Follows ImageProps href from react-native-svg that is used by react-native-qrcode-svg.
+ */
+ logo?: ImageSourcePropType;
+
+ /** The size ratio of logo to QR code */
+ logoRatio?: LogoRatio;
+
+ /** The size ratio of margin around logo to QR code */
+ logoMarginRatio?: LogoMarginRatio;
+
+ /** The QRCode size */
+ size?: number;
+
+ /** The QRCode color */
+ color?: string;
+
+ /** The QRCode background color */
+ backgroundColor?: string;
+
+ /**
+ * Function to retrieve the internal component ref and be able to call it's
+ * methods
+ */
+ getRef?: (ref: Ref) => Ref;
+};
+
+function QRCode({
+ url,
+ logo,
+ getRef,
+ size = 120,
+ color = defaultTheme.text,
+ backgroundColor = defaultTheme.highlightBG,
+ logoRatio = CONST.QR.DEFAULT_LOGO_SIZE_RATIO,
+ logoMarginRatio = CONST.QR.DEFAULT_LOGO_MARGIN_RATIO,
+}: QRCodeProps) {
+ return (
+
+ );
+}
+
+QRCode.displayName = 'QRCode';
+
+export default QRCode;
diff --git a/src/components/ReportActionItem/MoneyReportView.js b/src/components/ReportActionItem/MoneyReportView.js
index 9bc7a35f9fba..3f9b8bf53837 100644
--- a/src/components/ReportActionItem/MoneyReportView.js
+++ b/src/components/ReportActionItem/MoneyReportView.js
@@ -7,7 +7,6 @@ import styles from '../../styles/styles';
import themeColors from '../../styles/themes/default';
import * as ReportUtils from '../../libs/ReportUtils';
import * as StyleUtils from '../../styles/StyleUtils';
-import CONST from '../../CONST';
import Text from '../Text';
import Icon from '../Icon';
import * as Expensicons from '../Icon/Expensicons';
@@ -34,16 +33,16 @@ function MoneyReportView(props) {
const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(props.report);
const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend;
- const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.report.currency);
+ const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.report.currency, ReportUtils.hasOnlyDistanceRequestTransactions(props.report.reportID));
const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, props.report.currency);
const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, props.report.currency);
const subAmountTextStyles = [styles.taskTitleMenuItem, styles.alignSelfCenter, StyleUtils.getFontSizeStyle(variables.fontSizeh1), StyleUtils.getColorStyle(themeColors.textSupporting)];
return (
-
+
-
+
-
- {shouldShowBreakdown ? (
- <>
-
-
-
- {translate('cardTransactions.outOfPocket')}
-
-
-
-
- {formattedOutOfPocketAmount}
-
-
-
-
-
-
- {translate('cardTransactions.companySpend')}
-
+ {shouldShowBreakdown ? (
+ <>
+
+
+
+ {translate('cardTransactions.outOfPocket')}
+
+
+
+
+ {formattedOutOfPocketAmount}
+
+
-
-
- {formattedCompanySpendAmount}
-
+
+
+
+ {translate('cardTransactions.companySpend')}
+
+
+
+
+ {formattedCompanySpendAmount}
+
+
-
- >
- ) : undefined}
-
+ >
+ ) : undefined}
+
+
);
}
diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js
index 985d88e53d88..bdeec2640cdc 100644
--- a/src/components/ReportActionItem/ReportPreview.js
+++ b/src/components/ReportActionItem/ReportPreview.js
@@ -123,6 +123,7 @@ function ReportPreview(props) {
const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(props.iouReportID);
const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length;
const hasReceipts = transactionsWithReceipts.length > 0;
+ const hasOnlyDistanceRequests = ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID);
const isScanning = hasReceipts && ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action);
const hasErrors = hasReceipts && ReportUtils.hasMissingSmartscanFields(props.iouReportID);
const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3);
@@ -145,6 +146,9 @@ function ReportPreview(props) {
if (isScanning) {
return props.translate('iou.receiptScanning');
}
+ if (hasOnlyDistanceRequests) {
+ return props.translate('common.tbd');
+ }
// If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere")
let displayAmount = '';
@@ -241,9 +245,13 @@ function ReportPreview(props) {
onPress={(paymentType) => IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
- style={[styles.mt3]}
shouldShowPaymentOptions
- anchorAlignment={{
+ style={[styles.mt3]}
+ kycWallAnchorAlignment={{
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ }}
+ paymentMethodDropdownAnchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
}}
diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js
index 287f3210b14d..2989fd103850 100644
--- a/src/components/SettlementButton.js
+++ b/src/components/SettlementButton.js
@@ -70,8 +70,14 @@ const propTypes = {
/** Whether we should show a loading state for the main button */
isLoading: PropTypes.bool,
- /** The anchor alignment of the popover menu */
- anchorAlignment: PropTypes.shape({
+ /** The anchor alignment of the popover menu for payment method dropdown */
+ paymentMethodDropdownAnchorAlignment: PropTypes.shape({
+ horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
+ vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
+ }),
+
+ /** The anchor alignment of the popover menu for KYC wall popover */
+ kycWallAnchorAlignment: PropTypes.shape({
horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
}),
@@ -96,8 +102,12 @@ const defaultProps = {
policyID: '',
formattedAmount: '',
buttonSize: CONST.DROPDOWN_BUTTON_SIZE.MEDIUM,
- anchorAlignment: {
- horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
+ kycWallAnchorAlignment: {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, // button is at left, so horizontal anchor is at LEFT
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP
+ },
+ paymentMethodDropdownAnchorAlignment: {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, // caret for dropdown is at right, so horizontal anchor is at RIGHT
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP
},
};
@@ -105,7 +115,8 @@ const defaultProps = {
function SettlementButton({
addDebitCardRoute,
addBankAccountRoute,
- anchorAlignment,
+ kycWallAnchorAlignment,
+ paymentMethodDropdownAnchorAlignment,
betas,
buttonSize,
chatReportID,
@@ -210,7 +221,7 @@ function SettlementButton({
source={CONST.KYC_WALL_SOURCE.REPORT}
chatReportID={chatReportID}
iouReport={iouReport}
- anchorAlignment={anchorAlignment}
+ anchorAlignment={kycWallAnchorAlignment}
>
{(triggerKYCFlow, buttonRef) => (
)}
diff --git a/src/components/SwipeableView/index.js b/src/components/SwipeableView/index.js
deleted file mode 100644
index 96640b107608..000000000000
--- a/src/components/SwipeableView/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default ({children}) => children;
-
-// Swipeable View is available just on Android/iOS for now.
diff --git a/src/components/SwipeableView/index.native.js b/src/components/SwipeableView/index.native.tsx
similarity index 65%
rename from src/components/SwipeableView/index.native.js
rename to src/components/SwipeableView/index.native.tsx
index 2f1148721af1..ac500f025016 100644
--- a/src/components/SwipeableView/index.native.js
+++ b/src/components/SwipeableView/index.native.tsx
@@ -1,41 +1,34 @@
import React, {useRef} from 'react';
import {PanResponder, View} from 'react-native';
-import PropTypes from 'prop-types';
import CONST from '../../CONST';
+import SwipeableViewProps from './types';
-const propTypes = {
- children: PropTypes.element.isRequired,
-
- /** Callback to fire when the user swipes down on the child content */
- onSwipeDown: PropTypes.func.isRequired,
-};
-
-function SwipeableView(props) {
+function SwipeableView({children, onSwipeDown}: SwipeableViewProps) {
const minimumPixelDistance = CONST.COMPOSER_MAX_HEIGHT;
const oldYRef = useRef(0);
const panResponder = useRef(
PanResponder.create({
- // The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance
- // & swipe direction is downwards
+ // The PanResponder gets focus only when the y-axis movement is over minimumPixelDistance & swipe direction is downwards
+ // eslint-disable-next-line @typescript-eslint/naming-convention
onMoveShouldSetPanResponderCapture: (_event, gestureState) => {
if (gestureState.dy - oldYRef.current > 0 && gestureState.dy > minimumPixelDistance) {
return true;
}
oldYRef.current = gestureState.dy;
+ return false;
},
// Calls the callback when the swipe down is released; after the completion of the gesture
- onPanResponderRelease: props.onSwipeDown,
+ onPanResponderRelease: onSwipeDown,
}),
).current;
return (
// eslint-disable-next-line react/jsx-props-no-spreading
- {props.children}
+ {children}
);
}
-SwipeableView.propTypes = propTypes;
SwipeableView.displayName = 'SwipeableView';
export default SwipeableView;
diff --git a/src/components/SwipeableView/index.tsx b/src/components/SwipeableView/index.tsx
new file mode 100644
index 000000000000..335c3e7dcf03
--- /dev/null
+++ b/src/components/SwipeableView/index.tsx
@@ -0,0 +1,4 @@
+import SwipeableViewProps from './types';
+
+// Swipeable View is available just on Android/iOS for now.
+export default ({children}: SwipeableViewProps) => children;
diff --git a/src/components/SwipeableView/types.ts b/src/components/SwipeableView/types.ts
new file mode 100644
index 000000000000..560df7ef5a45
--- /dev/null
+++ b/src/components/SwipeableView/types.ts
@@ -0,0 +1,11 @@
+import {ReactNode} from 'react';
+
+type SwipeableViewProps = {
+ /** The content to be rendered within the SwipeableView */
+ children: ReactNode;
+
+ /** Callback to fire when the user swipes down on the child content */
+ onSwipeDown: () => void;
+};
+
+export default SwipeableViewProps;
diff --git a/src/components/ZeroWidthView/index.js b/src/components/ZeroWidthView/index.js
new file mode 100644
index 000000000000..6c3809a40a04
--- /dev/null
+++ b/src/components/ZeroWidthView/index.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import * as EmojiUtils from '../../libs/EmojiUtils';
+import * as Browser from '../../libs/Browser';
+import Text from '../Text';
+
+const propTypes = {
+ /** If this is the Concierge chat, we'll open the modal for requesting a setup call instead of showing popover menu */
+ text: PropTypes.string,
+
+ /** URL to the assigned guide's appointment booking calendar */
+ displayAsGroup: PropTypes.bool,
+};
+
+const defaultProps = {
+ text: '',
+ displayAsGroup: false,
+};
+
+function ZeroWidthView({text, displayAsGroup}) {
+ const firstLetterIsEmoji = EmojiUtils.isFirstLetterEmoji(text);
+ if (firstLetterIsEmoji && !displayAsGroup && !Browser.isMobile()) {
+ return ;
+ }
+ return null;
+}
+
+ZeroWidthView.propTypes = propTypes;
+ZeroWidthView.defaultProps = defaultProps;
+ZeroWidthView.displayName = 'ZeroWidthView';
+
+export default ZeroWidthView;
diff --git a/src/components/ZeroWidthView/index.native.js b/src/components/ZeroWidthView/index.native.js
new file mode 100644
index 000000000000..59c3cc74ab72
--- /dev/null
+++ b/src/components/ZeroWidthView/index.native.js
@@ -0,0 +1,5 @@
+function ZeroWidthView() {
+ return null;
+}
+
+export default ZeroWidthView;
diff --git a/src/components/withCurrentUserPersonalDetails.js b/src/components/withCurrentUserPersonalDetails.js
deleted file mode 100644
index 7a47ea7cc712..000000000000
--- a/src/components/withCurrentUserPersonalDetails.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, {useMemo} from 'react';
-import PropTypes from 'prop-types';
-import {withOnyx} from 'react-native-onyx';
-import getComponentDisplayName from '../libs/getComponentDisplayName';
-import ONYXKEYS from '../ONYXKEYS';
-import personalDetailsPropType from '../pages/personalDetailsPropType';
-import refPropTypes from './refPropTypes';
-
-const withCurrentUserPersonalDetailsPropTypes = {
- currentUserPersonalDetails: personalDetailsPropType,
-};
-
-const withCurrentUserPersonalDetailsDefaultProps = {
- currentUserPersonalDetails: {},
-};
-
-export default function (WrappedComponent) {
- const propTypes = {
- forwardedRef: refPropTypes,
-
- /** Personal details of all the users, including current user */
- personalDetails: PropTypes.objectOf(personalDetailsPropType),
-
- /** Session of the current user */
- session: PropTypes.shape({
- accountID: PropTypes.number,
- }),
- };
- const defaultProps = {
- forwardedRef: undefined,
- personalDetails: {},
- session: {
- accountID: 0,
- },
- };
-
- function WithCurrentUserPersonalDetails(props) {
- const accountID = props.session.accountID;
- const accountPersonalDetails = props.personalDetails[accountID];
- const currentUserPersonalDetails = useMemo(() => ({...accountPersonalDetails, accountID}), [accountPersonalDetails, accountID]);
- return (
-
- );
- }
-
- WithCurrentUserPersonalDetails.displayName = `WithCurrentUserPersonalDetails(${getComponentDisplayName(WrappedComponent)})`;
- WithCurrentUserPersonalDetails.propTypes = propTypes;
-
- WithCurrentUserPersonalDetails.defaultProps = defaultProps;
-
- const withCurrentUserPersonalDetails = React.forwardRef((props, ref) => (
-
- ));
-
- return withOnyx({
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- })(withCurrentUserPersonalDetails);
-}
-
-export {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps};
diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx
new file mode 100644
index 000000000000..e1472f280f17
--- /dev/null
+++ b/src/components/withCurrentUserPersonalDetails.tsx
@@ -0,0 +1,67 @@
+import React, {ComponentType, RefAttributes, ForwardedRef, useMemo} from 'react';
+import {OnyxEntry, withOnyx} from 'react-native-onyx';
+import getComponentDisplayName from '../libs/getComponentDisplayName';
+import ONYXKEYS from '../ONYXKEYS';
+import personalDetailsPropType from '../pages/personalDetailsPropType';
+import type {PersonalDetails, Session} from '../types/onyx';
+
+type CurrentUserPersonalDetails = PersonalDetails | Record;
+
+type OnyxProps = {
+ /** Personal details of all the users, including current user */
+ personalDetails: OnyxEntry>;
+
+ /** Session of the current user */
+ session: OnyxEntry;
+};
+
+type HOCProps = {
+ currentUserPersonalDetails: CurrentUserPersonalDetails;
+};
+
+type ComponentProps = OnyxProps & HOCProps;
+
+// TODO: remove when all components that use it will be migrated to TS
+const withCurrentUserPersonalDetailsPropTypes = {
+ currentUserPersonalDetails: personalDetailsPropType,
+};
+
+const withCurrentUserPersonalDetailsDefaultProps: HOCProps = {
+ currentUserPersonalDetails: {},
+};
+
+export default function (
+ WrappedComponent: ComponentType>,
+): ComponentType & RefAttributes, keyof OnyxProps>> {
+ function WithCurrentUserPersonalDetails(props: Omit, ref: ForwardedRef) {
+ const accountID = props.session?.accountID ?? 0;
+ const accountPersonalDetails = props.personalDetails?.[accountID];
+ const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo(
+ () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}),
+ [accountPersonalDetails, accountID],
+ );
+ return (
+
+ );
+ }
+
+ WithCurrentUserPersonalDetails.displayName = `WithCurrentUserPersonalDetails(${getComponentDisplayName(WrappedComponent)})`;
+
+ const withCurrentUserPersonalDetails = React.forwardRef(WithCurrentUserPersonalDetails);
+
+ return withOnyx & RefAttributes, OnyxProps>({
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+ })(withCurrentUserPersonalDetails);
+}
+
+export {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps};
diff --git a/src/components/withViewportOffsetTop.js b/src/components/withViewportOffsetTop.js
deleted file mode 100644
index ccf928b3bd13..000000000000
--- a/src/components/withViewportOffsetTop.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import React, {useEffect, forwardRef, useState} from 'react';
-import PropTypes from 'prop-types';
-import lodashGet from 'lodash/get';
-import getComponentDisplayName from '../libs/getComponentDisplayName';
-import addViewportResizeListener from '../libs/VisualViewport';
-import refPropTypes from './refPropTypes';
-
-const viewportOffsetTopPropTypes = {
- // viewportOffsetTop returns the offset of the top edge of the visual viewport from the
- // top edge of the layout viewport in CSS pixels, when the visual viewport is resized.
-
- viewportOffsetTop: PropTypes.number.isRequired,
-};
-
-export default function (WrappedComponent) {
- function WithViewportOffsetTop(props) {
- const [viewportOffsetTop, setViewportOffsetTop] = useState(0);
-
- useEffect(() => {
- /**
- * @param {SyntheticEvent} e
- */
- const updateDimensions = (e) => {
- const targetOffsetTop = lodashGet(e, 'target.offsetTop', 0);
- setViewportOffsetTop(targetOffsetTop);
- };
-
- const removeViewportResizeListener = addViewportResizeListener(updateDimensions);
-
- return () => {
- removeViewportResizeListener();
- };
- }, []);
-
- return (
-
- );
- }
-
- WithViewportOffsetTop.displayName = `WithViewportOffsetTop(${getComponentDisplayName(WrappedComponent)})`;
- WithViewportOffsetTop.propTypes = {
- forwardedRef: refPropTypes,
- };
- WithViewportOffsetTop.defaultProps = {
- forwardedRef: undefined,
- };
- return forwardRef((props, ref) => (
-
- ));
-}
-
-export {viewportOffsetTopPropTypes};
diff --git a/src/components/withViewportOffsetTop.tsx b/src/components/withViewportOffsetTop.tsx
new file mode 100644
index 000000000000..e2e1dc2d3484
--- /dev/null
+++ b/src/components/withViewportOffsetTop.tsx
@@ -0,0 +1,41 @@
+import React, {useEffect, forwardRef, useState, ComponentType, RefAttributes, ForwardedRef} from 'react';
+import getComponentDisplayName from '../libs/getComponentDisplayName';
+import addViewportResizeListener from '../libs/VisualViewport';
+
+type ViewportOffsetTopProps = {
+ // viewportOffsetTop returns the offset of the top edge of the visual viewport from the
+ // top edge of the layout viewport in CSS pixels, when the visual viewport is resized.
+ viewportOffsetTop: number;
+};
+
+export default function withViewportOffsetTop(WrappedComponent: ComponentType>) {
+ function WithViewportOffsetTop(props: Omit, ref: ForwardedRef) {
+ const [viewportOffsetTop, setViewportOffsetTop] = useState(0);
+
+ useEffect(() => {
+ const updateDimensions = (event: Event) => {
+ const targetOffsetTop = (event.target instanceof VisualViewport && event.target.offsetTop) || 0;
+ setViewportOffsetTop(targetOffsetTop);
+ };
+
+ const removeViewportResizeListener = addViewportResizeListener(updateDimensions);
+
+ return () => {
+ removeViewportResizeListener();
+ };
+ }, []);
+
+ return (
+
+ );
+ }
+
+ WithViewportOffsetTop.displayName = `WithViewportOffsetTop(${getComponentDisplayName(WrappedComponent)})`;
+
+ return forwardRef(WithViewportOffsetTop);
+}
diff --git a/src/hooks/useCopySelectionHelper.js b/src/hooks/useCopySelectionHelper.ts
similarity index 89%
rename from src/hooks/useCopySelectionHelper.js
rename to src/hooks/useCopySelectionHelper.ts
index 42871981e29c..b41bfb3c4aee 100644
--- a/src/hooks/useCopySelectionHelper.js
+++ b/src/hooks/useCopySelectionHelper.ts
@@ -25,10 +25,12 @@ export default function useCopySelectionHelper() {
copyShortcutConfig.shortcutKey,
copySelectionToClipboard,
copyShortcutConfig.descriptionKey,
- copyShortcutConfig.modifiers,
+ [...copyShortcutConfig.modifiers],
false,
);
- return unsubscribeCopyShortcut;
+ return () => {
+ unsubscribeCopyShortcut();
+ };
}, []);
}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 11637846130a..bc6bba610475 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -584,6 +584,7 @@ export default {
threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`,
threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`,
tagSelection: ({tagName}: TagSelectionParams) => `Select a ${tagName} to add additional organization to your money`,
+ categorySelection: 'Select a category to add additional organization to your money',
error: {
invalidAmount: 'Please enter a valid amount before continuing.',
invalidSplit: 'Split amounts do not equal total amount',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index e4a5c37241f2..df34eb0b1759 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -578,6 +578,7 @@ export default {
threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`,
tagSelection: ({tagName}: TagSelectionParams) => `Seleccione una ${tagName} para organizar mejor tu dinero`,
+ categorySelection: 'Seleccione una categoría 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',
diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts
index 21784d450a07..85ba8340c13e 100644
--- a/src/libs/CurrencyUtils.ts
+++ b/src/libs/CurrencyUtils.ts
@@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx';
import ONYXKEYS, {OnyxValues} from '../ONYXKEYS';
import CONST from '../CONST';
import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener';
+import * as Localize from './Localize';
import * as NumberFormatUtils from './NumberFormatUtils';
let currencyList: OnyxValues[typeof ONYXKEYS.CURRENCY_LIST] = {};
@@ -96,8 +97,13 @@ function convertToFrontendAmount(amountAsInt: number): number {
*
* @param amountInCents – should be an integer. Anything after a decimal place will be dropped.
* @param currency - IOU currency
+ * @param shouldFallbackToTbd - whether to return 'TBD' instead of a falsy value (e.g. 0.00)
*/
-function convertToDisplayString(amountInCents: number, currency: string = CONST.CURRENCY.USD): string {
+function convertToDisplayString(amountInCents: number, currency: string = CONST.CURRENCY.USD, shouldFallbackToTbd = false): string {
+ if (shouldFallbackToTbd && !amountInCents) {
+ return Localize.translateLocal('common.tbd');
+ }
+
const convertedAmount = convertToFrontendAmount(amountInCents);
return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, {
style: 'currency',
diff --git a/src/libs/Navigation/OnyxTabNavigator.js b/src/libs/Navigation/OnyxTabNavigator.js
index 2782054497b0..158160e9aa13 100644
--- a/src/libs/Navigation/OnyxTabNavigator.js
+++ b/src/libs/Navigation/OnyxTabNavigator.js
@@ -6,13 +6,13 @@ import Tab from '../actions/Tab';
import ONYXKEYS from '../../ONYXKEYS';
const propTypes = {
- /* ID of the tab component to be saved in onyx */
+ /** ID of the tab component to be saved in onyx */
id: PropTypes.string.isRequired,
- /* Name of the selected tab */
+ /** Name of the selected tab */
selectedTab: PropTypes.string,
- /* Children nodes */
+ /** Children nodes */
children: PropTypes.node.isRequired,
};
diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.js
index de902b53a7a4..2ecc818ebd23 100644
--- a/src/libs/PolicyUtils.js
+++ b/src/libs/PolicyUtils.js
@@ -161,7 +161,7 @@ const isPolicyAdmin = (policy) => lodashGet(policy, 'role') === CONST.POLICY.ROL
* @param {Object} policies
* @returns {Boolean}
*/
-const isPolicyMember = (policyID, policies) => _.some(policies, (policy) => policy.id === policyID);
+const isPolicyMember = (policyID, policies) => _.some(policies, (policy) => lodashGet(policy, 'id') === policyID);
/**
* @param {Object} policyMembers
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index c795e5d1c3b1..d016e220e147 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -1,4 +1,3 @@
-import {isEqual, max} from 'date-fns';
import _ from 'lodash';
import lodashFindLast from 'lodash/findLast';
import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
@@ -370,24 +369,19 @@ function replaceBaseURL(reportAction: ReportAction): ReportAction {
/**
*/
function getLastVisibleAction(reportID: string, actionsToMerge: ReportActions = {}): OnyxEntry {
- const updatedActionsToMerge: ReportActions = {};
+ let reportActions: ReportActions;
if (actionsToMerge && Object.keys(actionsToMerge).length !== 0) {
- Object.keys(actionsToMerge).forEach(
- (actionToMergeID) => (updatedActionsToMerge[actionToMergeID] = {...allReportActions?.[reportID]?.[actionToMergeID], ...actionsToMerge[actionToMergeID]}),
- );
- }
- const actions = Object.values({
- ...allReportActions?.[reportID],
- ...updatedActionsToMerge,
- });
- const visibleActions = actions.filter((action) => shouldReportActionBeVisibleAsLastAction(action));
-
- if (visibleActions.length === 0) {
+ reportActions = {...allReportActions?.[reportID]};
+ Object.keys(actionsToMerge).forEach((actionToMergeID) => (reportActions[actionToMergeID] = {...allReportActions?.[reportID]?.[actionToMergeID], ...actionsToMerge[actionToMergeID]}));
+ } else {
+ reportActions = allReportActions?.[reportID] ?? {};
+ }
+ const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action));
+ const sortedReportActions = getSortedReportActions(visibleReportActions, true);
+ if (sortedReportActions.length === 0) {
return null;
}
- const maxDate = max(visibleActions.map((action) => new Date(action.created)));
- const maxAction = visibleActions.find((action) => isEqual(new Date(action.created), maxDate));
- return maxAction ?? null;
+ return sortedReportActions[0];
}
function getLastVisibleMessage(reportID: string, actionsToMerge: ReportActions = {}): LastVisibleMessage {
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 5cf7396c6669..93774afc0aef 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -660,6 +660,17 @@ function hasSingleParticipant(report) {
return report && report.participantAccountIDs && report.participantAccountIDs.length === 1;
}
+/**
+ * Checks whether all the transactions linked to the IOU report are of the Distance Request type
+ *
+ * @param {string|null} iouReportID
+ * @returns {boolean}
+ */
+function hasOnlyDistanceRequestTransactions(iouReportID) {
+ const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID);
+ return _.all(allTransactions, (transaction) => TransactionUtils.isDistanceRequest(transaction));
+}
+
/**
* If the report is a thread and has a chat type set, it is a workspace chat.
*
@@ -1415,7 +1426,7 @@ function getPolicyExpenseChatName(report, policy = undefined) {
}
/**
- * Get the title for a IOU or expense chat which will be showing the payer and the amount
+ * Get the title for an IOU or expense chat which will be showing the payer and the amount
*
* @param {Object} report
* @param {Object} [policy]
@@ -1423,15 +1434,15 @@ function getPolicyExpenseChatName(report, policy = undefined) {
*/
function getMoneyRequestReportName(report, policy = undefined) {
const moneyRequestTotal = getMoneyRequestReimbursableTotal(report);
- const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report.currency);
+ const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report.currency, hasOnlyDistanceRequestTransactions(report.reportID));
const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID);
- const payerPaidAmountMesssage = Localize.translateLocal('iou.payerPaidAmount', {
+ const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', {
payer: payerName,
amount: formattedAmount,
});
if (report.isWaitingOnBankAccount) {
- return `${payerPaidAmountMesssage} • ${Localize.translateLocal('iou.pending')}`;
+ return `${payerPaidAmountMessage} • ${Localize.translateLocal('iou.pending')}`;
}
if (hasNonReimbursableTransactions(report.reportID)) {
@@ -1442,7 +1453,7 @@ function getMoneyRequestReportName(report, policy = undefined) {
return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount});
}
- return payerPaidAmountMesssage;
+ return payerPaidAmountMessage;
}
/**
@@ -1527,7 +1538,7 @@ function canEditReportAction(reportAction) {
/**
* Gets all transactions on an IOU report with a receipt
*
- * @param {Object|null} iouReportID
+ * @param {string|null} iouReportID
* @returns {[Object]}
*/
function getTransactionsWithReceipts(iouReportID) {
@@ -1593,7 +1604,7 @@ function getTransactionReportName(reportAction) {
const {amount, currency, comment} = getTransactionDetails(transaction);
return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', {
- formattedAmount: CurrencyUtils.convertToDisplayString(amount, currency),
+ formattedAmount: CurrencyUtils.convertToDisplayString(amount, currency, TransactionUtils.isDistanceRequest(transaction)),
comment,
});
}
@@ -3392,8 +3403,16 @@ function parseReportRouteParams(route) {
}
const pathSegments = parsingRoute.split('/');
+
+ const reportIDSegment = pathSegments[1];
+
+ // Check for "undefined" or any other unwanted string values
+ if (!reportIDSegment || reportIDSegment === 'undefined') {
+ return {reportID: '', isSubReportPageRoute: false};
+ }
+
return {
- reportID: pathSegments[1],
+ reportID: reportIDSegment,
isSubReportPageRoute: pathSegments.length > 2,
};
}
@@ -3710,6 +3729,21 @@ function getPolicyExpenseChatReportIDByOwner(policyOwner) {
return expenseChat.reportID;
}
+/**
+ * Check if the report can create the request with type is iouType
+ * @param {Object} report
+ * @param {Array} betas
+ * @param {String} iouType
+ * @returns {Boolean}
+ */
+function canCreateRequest(report, betas, iouType) {
+ const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []);
+ if (shouldDisableWriteActions(report)) {
+ return false;
+ }
+ return getMoneyRequestOptions(report, participantAccountIDs, betas).includes(iouType);
+}
+
/**
* @param {String} policyID
* @param {Array} accountIDs
@@ -4039,6 +4073,7 @@ export {
getCommentLength,
getParsedComment,
getMoneyRequestOptions,
+ canCreateRequest,
hasIOUWaitingOnCurrentUserBankAccount,
canRequestMoney,
getWhisperDisplayNames,
@@ -4079,6 +4114,7 @@ export {
buildTransactionThread,
areAllRequestsBeingSmartScanned,
getTransactionsWithReceipts,
+ hasOnlyDistanceRequestTransactions,
hasNonReimbursableTransactions,
hasMissingSmartscanFields,
getIOUReportActionDisplayMessage,
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 5f1114d4b03e..44f8094ca13d 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -414,7 +414,9 @@ function getWaypointIndex(key: string): number {
* Filters the waypoints which are valid and returns those
*/
function getValidWaypoints(waypoints: WaypointCollection, reArrangeIndexes = false): WaypointCollection {
- const sortedIndexes = Object.keys(waypoints).map(getWaypointIndex).sort();
+ const sortedIndexes = Object.keys(waypoints)
+ .map(getWaypointIndex)
+ .sort((a, b) => a - b);
const waypointValues = sortedIndexes.map((index) => waypoints[`waypoint${index}`]);
// Ensure the number of waypoints is between 2 and 25
if (waypointValues.length < 2 || waypointValues.length > 25) {
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
index bf4f170f1ba7..cc755f10dfc6 100644
--- a/src/libs/actions/BankAccounts.ts
+++ b/src/libs/actions/BankAccounts.ts
@@ -145,7 +145,7 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc
/**
* Adds a bank account via Plaid
*
- * @TODO offline pattern for this command will have to be added later once the pattern B design doc is complete
+ * TODO: offline pattern for this command will have to be added later once the pattern B design doc is complete
*/
function addPersonalBankAccount(account: PlaidBankAccount) {
const commandName = 'AddPersonalBankAccount';
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 07e814f92884..a157c54c8002 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -1054,7 +1054,8 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(participant);
// In case the participant is a workspace, email & accountID should remain undefined and won't be used in the rest of this code
- const email = isOwnPolicyExpenseChat || isPolicyExpenseChat ? '' : OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login).toLowerCase();
+ // participant.login is undefined when the request is initiated from a group DM with an unknown user, so we need to add a default
+ const email = isOwnPolicyExpenseChat || isPolicyExpenseChat ? '' : OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login || '').toLowerCase();
const accountID = isOwnPolicyExpenseChat || isPolicyExpenseChat ? 0 : Number(participant.accountID);
if (email === currentUserEmailForIOUSplit) {
return;
diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js
index e4a1b763cd62..090e6205e925 100755
--- a/src/pages/DetailsPage.js
+++ b/src/pages/DetailsPage.js
@@ -89,8 +89,6 @@ function DetailsPage(props) {
let details = _.find(props.personalDetails, (detail) => detail.login === login.toLowerCase());
if (!details) {
- // TODO: these personal details aren't in my local test account but are in
- // my staging account, i wonder why!
if (login === CONST.EMAIL.CONCIERGE) {
details = {
accountID: CONST.ACCOUNT_ID.CONCIERGE,
diff --git a/src/pages/EditRequestCategoryPage.js b/src/pages/EditRequestCategoryPage.js
index e47935dd9df1..c0db5a16b140 100644
--- a/src/pages/EditRequestCategoryPage.js
+++ b/src/pages/EditRequestCategoryPage.js
@@ -4,6 +4,8 @@ import ScreenWrapper from '../components/ScreenWrapper';
import HeaderWithBackButton from '../components/HeaderWithBackButton';
import Navigation from '../libs/Navigation/Navigation';
import useLocalize from '../hooks/useLocalize';
+import styles from '../styles/styles';
+import Text from '../components/Text';
import CategoryPicker from '../components/CategoryPicker';
const propTypes = {
@@ -36,7 +38,7 @@ function EditRequestCategoryPage({defaultCategory, policyID, onSubmit}) {
title={translate('common.category')}
onBackButtonPress={Navigation.goBack}
/>
-
+ {translate('iou.categorySelection')}
-
+ {translate('iou.tagSelection', {tagName: tagName || translate('common.tag')})}
`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`,
diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js
index c923a8d96d70..5021ccdc42d7 100644
--- a/src/pages/RoomInvitePage.js
+++ b/src/pages/RoomInvitePage.js
@@ -250,7 +250,7 @@ RoomInvitePage.defaultProps = defaultProps;
RoomInvitePage.displayName = 'RoomInvitePage';
export default compose(
- withReportOrNotFound,
+ withReportOrNotFound(),
withOnyx({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
diff --git a/src/pages/RoomMembersPage.js b/src/pages/RoomMembersPage.js
index 87e1afab8ae9..3a6e3b6fd90f 100644
--- a/src/pages/RoomMembersPage.js
+++ b/src/pages/RoomMembersPage.js
@@ -316,7 +316,7 @@ RoomMembersPage.displayName = 'RoomMembersPage';
export default compose(
withLocalize,
withWindowDimensions,
- withReportOrNotFound,
+ withReportOrNotFound(),
withOnyx({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js
index 4eaf1c1ce15c..81000c2dab92 100644
--- a/src/pages/home/ReportScreen.js
+++ b/src/pages/home/ReportScreen.js
@@ -26,7 +26,7 @@ import Banner from '../../components/Banner';
import reportPropTypes from '../reportPropTypes';
import reportMetadataPropTypes from '../reportMetadataPropTypes';
import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
-import withViewportOffsetTop, {viewportOffsetTopPropTypes} from '../../components/withViewportOffsetTop';
+import withViewportOffsetTop from '../../components/withViewportOffsetTop';
import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
import personalDetailsPropType from '../personalDetailsPropType';
import getIsReportFullyVisible from '../../libs/getIsReportFullyVisible';
@@ -94,7 +94,7 @@ const propTypes = {
/** Whether user is leaving the current report */
userLeavingStatus: PropTypes.bool,
- ...viewportOffsetTopPropTypes,
+ viewportOffsetTop: PropTypes.number.isRequired,
...withCurrentReportIDPropTypes,
};
@@ -317,7 +317,7 @@ function ReportScreen({
prevOnyxReportID === routeReportID &&
!onyxReportID &&
prevReport.statusNum === CONST.REPORT.STATUS.OPEN &&
- (report.statusNum === CONST.REPORT.STATUS.CLOSED || !report.statusNum))
+ (report.statusNum === CONST.REPORT.STATUS.CLOSED || (!report.statusNum && !prevReport.parentReportID)))
) {
Navigation.dismissModal();
if (Navigation.getTopmostReportId() === prevOnyxReportID) {
diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js
index ffd7f65185ce..e194d0870885 100644
--- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js
+++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js
@@ -566,6 +566,7 @@ function ComposerWithSuggestions({
{
- if (shouldBlockCalc.current || selectionEnd < 1) {
+ if (shouldBlockCalc.current || selectionEnd < 1 || !isComposerFocused) {
shouldBlockCalc.current = false;
resetSuggestions();
return;
@@ -229,7 +230,7 @@ function SuggestionMention({
}));
setHighlightedMentionIndex(0);
},
- [getMentionOptions, personalDetails, resetSuggestions, setHighlightedMentionIndex, value],
+ [getMentionOptions, personalDetails, resetSuggestions, setHighlightedMentionIndex, value, isComposerFocused],
);
useEffect(() => {
diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.js b/src/pages/home/report/ReportActionCompose/Suggestions.js
index 60c31efb1446..74e9e79471e7 100644
--- a/src/pages/home/report/ReportActionCompose/Suggestions.js
+++ b/src/pages/home/report/ReportActionCompose/Suggestions.js
@@ -40,6 +40,7 @@ function Suggestions({
resetKeyboardInput,
measureParentContainer,
isAutoSuggestionPickerLarge,
+ isComposerFocused,
}) {
const suggestionEmojiRef = useRef(null);
const suggestionMentionRef = useRef(null);
@@ -103,6 +104,7 @@ function Suggestions({
composerHeight,
isAutoSuggestionPickerLarge,
measureParentContainer,
+ isComposerFocused,
};
return (
diff --git a/src/pages/home/report/ReportActionCompose/suggestionProps.js b/src/pages/home/report/ReportActionCompose/suggestionProps.js
index 815a1c5619f5..62c29f3d418e 100644
--- a/src/pages/home/report/ReportActionCompose/suggestionProps.js
+++ b/src/pages/home/report/ReportActionCompose/suggestionProps.js
@@ -24,6 +24,9 @@ const baseProps = {
/** Meaures the parent container's position and dimensions. */
measureParentContainer: PropTypes.func.isRequired,
+
+ /** Report composer focus state */
+ isComposerFocused: PropTypes.bool,
};
const implementationBaseProps = {
diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js
index 0b6333e31ef8..57b51ef50519 100644
--- a/src/pages/home/report/ReportActionItemFragment.js
+++ b/src/pages/home/report/ReportActionItemFragment.js
@@ -18,7 +18,7 @@ import CONST from '../../../CONST';
import editedLabelStyles from '../../../styles/editedLabelStyles';
import UserDetailsTooltip from '../../../components/UserDetailsTooltip';
import avatarPropTypes from '../../../components/avatarPropTypes';
-import * as Browser from '../../../libs/Browser';
+import ZeroWidthView from '../../../components/ZeroWidthView';
const propTypes = {
/** Users accountID */
@@ -90,24 +90,6 @@ const defaultProps = {
};
function ReportActionItemFragment(props) {
- /**
- * Checks text element for presence of emoji as first character
- * and insert Zero-Width character to avoid selection issue
- * mentioned here https://github.com/Expensify/App/issues/29021
- *
- * @param {String} text
- * @param {Boolean} displayAsGroup
- * @returns {ReactNode | null} Text component with zero width character
- */
-
- const checkForEmojiForSelection = (text, displayAsGroup) => {
- const firstLetterIsEmoji = EmojiUtils.isFirstLetterEmoji(text);
- if (firstLetterIsEmoji && !displayAsGroup && !Browser.isMobile()) {
- return ;
- }
- return null;
- };
-
switch (props.fragment.type) {
case 'COMMENT': {
const {html, text} = props.fragment;
@@ -139,7 +121,10 @@ function ReportActionItemFragment(props) {
return (
- {checkForEmojiForSelection(text, props.displayAsGroup)}
+
>
diff --git a/src/pages/home/report/ReportDetailsShareCodePage.js b/src/pages/home/report/ReportDetailsShareCodePage.js
index 62030f004bc6..7c22726ac82b 100644
--- a/src/pages/home/report/ReportDetailsShareCodePage.js
+++ b/src/pages/home/report/ReportDetailsShareCodePage.js
@@ -28,4 +28,4 @@ function ReportDetailsShareCodePage(props) {
ReportDetailsShareCodePage.propTypes = propTypes;
ReportDetailsShareCodePage.defaultProps = defaultProps;
-export default withReportOrNotFound(ReportDetailsShareCodePage);
+export default withReportOrNotFound()(ReportDetailsShareCodePage);
diff --git a/src/pages/home/report/withReportOrNotFound.js b/src/pages/home/report/withReportOrNotFound.js
index 5829ac7a6015..891e2e2418c6 100644
--- a/src/pages/home/report/withReportOrNotFound.js
+++ b/src/pages/home/report/withReportOrNotFound.js
@@ -9,7 +9,7 @@ import reportPropTypes from '../../reportPropTypes';
import FullscreenLoadingIndicator from '../../../components/FullscreenLoadingIndicator';
import * as ReportUtils from '../../../libs/ReportUtils';
-export default function (WrappedComponent) {
+export default function (shouldRequireReportID = true) {
const propTypes = {
/** The HOC takes an optional ref as a prop and passes it as a ref to the wrapped component.
* That way, if a ref is passed to a component wrapped in the HOC, the ref is a reference to the wrapped component, not the HOC. */
@@ -29,6 +29,14 @@ export default function (WrappedComponent) {
}),
),
+ /** Route params */
+ route: PropTypes.shape({
+ params: PropTypes.shape({
+ /** Report ID passed via route */
+ reportID: PropTypes.string,
+ }),
+ }).isRequired,
+
/** Beta features list */
betas: PropTypes.arrayOf(PropTypes.string),
@@ -44,67 +52,74 @@ export default function (WrappedComponent) {
isLoadingReportData: true,
};
- // eslint-disable-next-line rulesdir/no-negated-variables
- function WithReportOrNotFound(props) {
- const contentShown = React.useRef(false);
-
- const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData && (_.isEmpty(props.report) || !props.report.reportID);
+ return (WrappedComponent) => {
// eslint-disable-next-line rulesdir/no-negated-variables
- const shouldShowNotFoundPage = _.isEmpty(props.report) || !props.report.reportID || !ReportUtils.canAccessReport(props.report, props.policies, props.betas);
-
- // If the content was shown but it's not anymore that means the report was deleted and we are probably navigating out of this screen.
- // Return null for this case to avoid rendering FullScreenLoadingIndicator or NotFoundPage when animating transition.
- if (shouldShowNotFoundPage && contentShown.current) {
- return null;
+ function WithReportOrNotFound(props) {
+ const contentShown = React.useRef(false);
+
+ const isReportIdInRoute = !_.isUndefined(props.route.params.reportID);
+
+ // If we should require reportID or we have a reportID in the route, we will check the reportID is valid or not
+ if (shouldRequireReportID || isReportIdInRoute) {
+ const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData && (_.isEmpty(props.report) || !props.report.reportID);
+ // eslint-disable-next-line rulesdir/no-negated-variables
+ const shouldShowNotFoundPage = _.isEmpty(props.report) || !props.report.reportID || !ReportUtils.canAccessReport(props.report, props.policies, props.betas);
+
+ // If the content was shown but it's not anymore that means the report was deleted and we are probably navigating out of this screen.
+ // Return null for this case to avoid rendering FullScreenLoadingIndicator or NotFoundPage when animating transition.
+ if (shouldShowNotFoundPage && contentShown.current) {
+ return null;
+ }
+
+ if (shouldShowFullScreenLoadingIndicator) {
+ return ;
+ }
+
+ if (shouldShowNotFoundPage) {
+ return ;
+ }
+ }
+
+ if (!contentShown.current) {
+ contentShown.current = true;
+ }
+
+ const rest = _.omit(props, ['forwardedRef']);
+ return (
+
+ );
}
- if (shouldShowFullScreenLoadingIndicator) {
- return ;
- }
+ WithReportOrNotFound.propTypes = propTypes;
+ WithReportOrNotFound.defaultProps = defaultProps;
+ WithReportOrNotFound.displayName = `withReportOrNotFound(${getComponentDisplayName(WrappedComponent)})`;
- if (shouldShowNotFoundPage) {
- return ;
- }
-
- if (!contentShown.current) {
- contentShown.current = true;
- }
-
- const rest = _.omit(props, ['forwardedRef']);
- return (
- (
+
- );
- }
-
- WithReportOrNotFound.propTypes = propTypes;
- WithReportOrNotFound.defaultProps = defaultProps;
- WithReportOrNotFound.displayName = `withReportOrNotFound(${getComponentDisplayName(WrappedComponent)})`;
-
- // eslint-disable-next-line rulesdir/no-negated-variables
- const withReportOrNotFound = React.forwardRef((props, ref) => (
-
- ));
-
- return withOnyx({
- report: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
- },
- isLoadingReportData: {
- key: ONYXKEYS.IS_LOADING_REPORT_DATA,
- },
- betas: {
- key: ONYXKEYS.BETAS,
- },
- policies: {
- key: ONYXKEYS.COLLECTION.POLICY,
- },
- })(withReportOrNotFound);
+ ));
+
+ return withOnyx({
+ report: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
+ },
+ isLoadingReportData: {
+ key: ONYXKEYS.IS_LOADING_REPORT_DATA,
+ },
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+ })(withReportOrNotFound);
+ };
}
diff --git a/src/pages/iou/MoneyRequestCategoryPage.js b/src/pages/iou/MoneyRequestCategoryPage.js
index 5055c9f90f8a..7431a5deed77 100644
--- a/src/pages/iou/MoneyRequestCategoryPage.js
+++ b/src/pages/iou/MoneyRequestCategoryPage.js
@@ -12,6 +12,8 @@ import CategoryPicker from '../../components/CategoryPicker';
import ONYXKEYS from '../../ONYXKEYS';
import reportPropTypes from '../reportPropTypes';
import * as IOU from '../../libs/actions/IOU';
+import styles from '../../styles/styles';
+import Text from '../../components/Text';
import {iouPropTypes, iouDefaultProps} from './propTypes';
const propTypes = {
@@ -70,7 +72,7 @@ function MoneyRequestCategoryPage({route, report, iou}) {
title={translate('common.category')}
onBackButtonPress={navigateBack}
/>
-
+ {translate('iou.categorySelection')}
{
@@ -89,7 +98,7 @@ function MoneyRequestSelectorPage(props) {
testID={MoneyRequestSelectorPage.displayName}
>
{({safeAreaPaddingBottomStyle}) => (
-
+
`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
- },
- selectedTab: {
- key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`,
- },
-})(MoneyRequestSelectorPage);
+export default compose(
+ withReportOrNotFound(false),
+ withOnyx({
+ selectedTab: {
+ key: `${ONYXKEYS.SELECTED_TAB}_${CONST.TAB.RECEIPT_TAB_ID}`,
+ },
+ }),
+)(MoneyRequestSelectorPage);
diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js
index a58f41e5e693..1c48a4f1a44a 100644
--- a/src/pages/iou/SplitBillDetailsPage.js
+++ b/src/pages/iou/SplitBillDetailsPage.js
@@ -145,6 +145,7 @@ function SplitBillDetailsPage(props) {
reportActionID={reportAction.reportActionID}
transactionID={props.transaction.transactionID}
onConfirm={onConfirm}
+ isPolicyExpenseChat={ReportUtils.isPolicyExpenseChat(props.report)}
/>
)}
diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js
index df10c5b4d609..8b697cde4880 100644
--- a/src/pages/iou/steps/MoneyRequestConfirmPage.js
+++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js
@@ -127,14 +127,14 @@ function MoneyRequestConfirmPage(props) {
IOU.resetMoneyRequestInfo(moneyRequestId);
}
- if (_.isEmpty(props.iou.participants) || (props.iou.amount === 0 && !props.iou.receiptPath && !isDistanceRequest) || shouldReset) {
+ if (_.isEmpty(props.iou.participants) || (props.iou.amount === 0 && !props.iou.receiptPath && !isDistanceRequest) || shouldReset || ReportUtils.isArchivedRoom(props.report)) {
Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType.current, reportID.current), true);
}
return () => {
prevMoneyRequestId.current = props.iou.id;
};
- }, [props.iou.participants, props.iou.amount, props.iou.id, props.iou.receiptPath, isDistanceRequest]);
+ }, [props.iou.participants, props.iou.amount, props.iou.id, props.iou.receiptPath, isDistanceRequest, props.report]);
const navigateBack = () => {
let fallback;
diff --git a/src/pages/iou/steps/NewRequestAmountPage.js b/src/pages/iou/steps/NewRequestAmountPage.js
index ae319f5a73bb..15a2c74d8a95 100644
--- a/src/pages/iou/steps/NewRequestAmountPage.js
+++ b/src/pages/iou/steps/NewRequestAmountPage.js
@@ -8,7 +8,6 @@ import _ from 'underscore';
import ONYXKEYS from '../../../ONYXKEYS';
import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
-import * as ReportUtils from '../../../libs/ReportUtils';
import * as CurrencyUtils from '../../../libs/CurrencyUtils';
import reportPropTypes from '../../reportPropTypes';
import * as IOU from '../../../libs/actions/IOU';
@@ -83,14 +82,6 @@ function NewRequestAmountPage({route, iou, report, selectedTab}) {
}, []),
);
- // Check and dismiss modal
- useEffect(() => {
- if (!ReportUtils.shouldDisableWriteActions(report)) {
- return;
- }
- Navigation.dismissModal(reportID);
- }, [report, reportID]);
-
// Because we use Onyx to store IOU info, when we try to make two different money requests from different tabs,
// it can result in an IOU sent with improper values. In such cases we want to reset the flow and redirect the user to the first step of the IOU.
useEffect(() => {
diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js
index cce43117d4f2..480c425a9094 100644
--- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.js
+++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.js
@@ -122,7 +122,7 @@ function NewContactMethodPage(props) {
ref={(el) => (loginInputRef.current = el)}
inputID="phoneOrEmail"
autoCapitalize="none"
- returnKeyType="done"
+ returnKeyType="go"
maxLength={CONST.LOGIN_CHARACTER_LIMIT}
/>
diff --git a/src/pages/settings/Report/NotificationPreferencePage.js b/src/pages/settings/Report/NotificationPreferencePage.js
index 64e6bdfb4b5b..43e346150ca8 100644
--- a/src/pages/settings/Report/NotificationPreferencePage.js
+++ b/src/pages/settings/Report/NotificationPreferencePage.js
@@ -56,4 +56,4 @@ function NotificationPreferencePage(props) {
NotificationPreferencePage.displayName = 'NotificationPreferencePage';
NotificationPreferencePage.propTypes = propTypes;
-export default compose(withLocalize, withReportOrNotFound)(NotificationPreferencePage);
+export default compose(withLocalize, withReportOrNotFound())(NotificationPreferencePage);
diff --git a/src/pages/settings/Report/ReportSettingsPage.js b/src/pages/settings/Report/ReportSettingsPage.js
index 9ec6eb071de2..fb88cbd59f25 100644
--- a/src/pages/settings/Report/ReportSettingsPage.js
+++ b/src/pages/settings/Report/ReportSettingsPage.js
@@ -209,7 +209,7 @@ ReportSettingsPage.propTypes = propTypes;
ReportSettingsPage.defaultProps = defaultProps;
ReportSettingsPage.displayName = 'ReportSettingsPage';
export default compose(
- withReportOrNotFound,
+ withReportOrNotFound(),
withOnyx({
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js
index 985d83e7fd95..4ce997533378 100644
--- a/src/pages/settings/Report/RoomNamePage.js
+++ b/src/pages/settings/Report/RoomNamePage.js
@@ -118,7 +118,7 @@ RoomNamePage.displayName = 'RoomNamePage';
export default compose(
withLocalize,
- withReportOrNotFound,
+ withReportOrNotFound(),
withOnyx({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
diff --git a/src/pages/settings/Report/WriteCapabilityPage.js b/src/pages/settings/Report/WriteCapabilityPage.js
index 1558d98a830a..174cc57d8d18 100644
--- a/src/pages/settings/Report/WriteCapabilityPage.js
+++ b/src/pages/settings/Report/WriteCapabilityPage.js
@@ -67,7 +67,7 @@ WriteCapabilityPage.defaultProps = defaultProps;
export default compose(
withLocalize,
- withReportOrNotFound,
+ withReportOrNotFound(),
withOnyx({
policy: {
key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`,
diff --git a/src/pages/settings/Wallet/WalletEmptyState.js b/src/pages/settings/Wallet/WalletEmptyState.js
index adfd2cf49cee..f54716e3110a 100644
--- a/src/pages/settings/Wallet/WalletEmptyState.js
+++ b/src/pages/settings/Wallet/WalletEmptyState.js
@@ -9,6 +9,7 @@ import ROUTES from '../../../ROUTES';
import * as Illustrations from '../../../components/Icon/Illustrations';
import FeatureList from '../../../components/FeatureList';
import themeColors from '../../../styles/themes/default';
+import SCREENS from '../../../SCREENS';
const propTypes = {
/** The function that is called when a menu item is pressed */
@@ -34,7 +35,7 @@ function WalletEmptyState({onAddPaymentMethod}) {
const {translate} = useLocalize();
return (
Navigation.goBack(ROUTES.SETTINGS)}
title={translate('common.wallet')}
diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js
index 37bb49952984..d1a2e4780f0d 100644
--- a/src/pages/settings/Wallet/WalletPage/WalletPage.js
+++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js
@@ -6,7 +6,7 @@ import PaymentMethodList from '../PaymentMethodList';
import ROUTES from '../../../../ROUTES';
import HeaderWithBackButton from '../../../../components/HeaderWithBackButton';
import ScreenWrapper from '../../../../components/ScreenWrapper';
-import Navigation, {navigationRef} from '../../../../libs/Navigation/Navigation';
+import Navigation from '../../../../libs/Navigation/Navigation';
import styles from '../../../../styles/styles';
import compose from '../../../../libs/compose';
import * as BankAccounts from '../../../../libs/actions/BankAccounts';
@@ -61,7 +61,7 @@ function WalletPage({bankAccountList, betas, cardList, fundList, isLoadingPaymen
const [showConfirmDeleteContent, setShowConfirmDeleteContent] = useState(false);
const hasBankAccount = !_.isEmpty(bankAccountList) || !_.isEmpty(fundList);
- const hasWallet = userWallet.walletLinkedAccountID > 0;
+ const hasWallet = !_.isEmpty(userWallet);
const hasActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], userWallet.tierName);
const hasAssignedCard = !_.isEmpty(cardList);
const shouldShowEmptyState = !hasBankAccount && !hasWallet && !hasAssignedCard;
@@ -299,13 +299,6 @@ function WalletPage({bankAccountList, betas, cardList, fundList, isLoadingPaymen
}
}, [hideDefaultDeleteMenu, paymentMethod.methodID, paymentMethod.selectedPaymentMethodType, bankAccountList, fundList, shouldShowDefaultDeleteMenu]);
- useEffect(() => {
- if (!shouldShowEmptyState) {
- return;
- }
- navigationRef.setParams({backgroundColor: themeColors.walletPageBG});
- }, [shouldShowEmptyState]);
-
const shouldShowMakeDefaultButton =
!paymentMethod.isSelectedPaymentMethodDefault &&
Permissions.canUseWallet(betas) &&
diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js
index 3576f92be31f..8adedde6d546 100644
--- a/src/pages/signin/LoginForm/BaseLoginForm.js
+++ b/src/pages/signin/LoginForm/BaseLoginForm.js
@@ -213,6 +213,7 @@ function LoginForm(props) {
accessibilityLabel={translate('loginForm.phoneOrEmail')}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
value={login}
+ returnKeyType="go"
autoCompleteType="username"
textContentType="username"
nativeID="username"
diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js
index be2cdad03fe6..01ed7e55f8a0 100644
--- a/src/pages/tasks/TaskDescriptionPage.js
+++ b/src/pages/tasks/TaskDescriptionPage.js
@@ -130,7 +130,7 @@ TaskDescriptionPage.displayName = 'TaskDescriptionPage';
export default compose(
withLocalize,
withCurrentUserPersonalDetails,
- withReportOrNotFound,
+ withReportOrNotFound(),
withOnyx({
session: {
key: ONYXKEYS.SESSION,
diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.js
index fca90a5fa904..32cd6c3d2cc3 100644
--- a/src/pages/tasks/TaskTitlePage.js
+++ b/src/pages/tasks/TaskTitlePage.js
@@ -125,7 +125,7 @@ TaskTitlePage.displayName = 'TaskTitlePage';
export default compose(
withLocalize,
withCurrentUserPersonalDetails,
- withReportOrNotFound,
+ withReportOrNotFound(),
withOnyx({
session: {
key: ONYXKEYS.SESSION,
diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts
index 62da2bf3be4b..48100b2efb60 100644
--- a/src/styles/StyleUtils.ts
+++ b/src/styles/StyleUtils.ts
@@ -882,17 +882,19 @@ function getErrorPageContainerStyle(safeAreaPaddingBottom = 0): ViewStyle {
/**
* Gets the correct size for the empty state background image based on screen dimensions
*/
-function getReportWelcomeBackgroundImageStyle(isSmallScreenWidth: boolean): ViewStyle {
+function getReportWelcomeBackgroundImageStyle(isSmallScreenWidth: boolean, isMoneyReport = false): ViewStyle {
+ const emptyStateBackground = isMoneyReport ? CONST.EMPTY_STATE_BACKGROUND.MONEY_REPORT : CONST.EMPTY_STATE_BACKGROUND;
+
if (isSmallScreenWidth) {
return {
- height: CONST.EMPTY_STATE_BACKGROUND.SMALL_SCREEN.IMAGE_HEIGHT,
+ height: emptyStateBackground.SMALL_SCREEN.IMAGE_HEIGHT,
width: '200%',
position: 'absolute',
};
}
return {
- height: CONST.EMPTY_STATE_BACKGROUND.WIDE_SCREEN.IMAGE_HEIGHT,
+ height: emptyStateBackground.WIDE_SCREEN.IMAGE_HEIGHT,
width: '100%',
position: 'absolute',
};
@@ -901,15 +903,16 @@ function getReportWelcomeBackgroundImageStyle(isSmallScreenWidth: boolean): View
/**
* Gets the correct top margin size for the chat welcome message based on screen dimensions
*/
-function getReportWelcomeTopMarginStyle(isSmallScreenWidth: boolean): ViewStyle {
+function getReportWelcomeTopMarginStyle(isSmallScreenWidth: boolean, isMoneyReport = false): ViewStyle {
+ const emptyStateBackground = isMoneyReport ? CONST.EMPTY_STATE_BACKGROUND.MONEY_REPORT : CONST.EMPTY_STATE_BACKGROUND;
if (isSmallScreenWidth) {
return {
- marginTop: CONST.EMPTY_STATE_BACKGROUND.SMALL_SCREEN.VIEW_HEIGHT,
+ marginTop: emptyStateBackground.SMALL_SCREEN.VIEW_HEIGHT,
};
}
return {
- marginTop: CONST.EMPTY_STATE_BACKGROUND.WIDE_SCREEN.VIEW_HEIGHT,
+ marginTop: emptyStateBackground.WIDE_SCREEN.VIEW_HEIGHT,
};
}
@@ -934,17 +937,18 @@ function getLineHeightStyle(lineHeight: number): TextStyle {
/**
* Gets the correct size for the empty state container based on screen dimensions
*/
-function getReportWelcomeContainerStyle(isSmallScreenWidth: boolean): ViewStyle {
+function getReportWelcomeContainerStyle(isSmallScreenWidth: boolean, isMoneyReport = false): ViewStyle {
+ const emptyStateBackground = isMoneyReport ? CONST.EMPTY_STATE_BACKGROUND.MONEY_REPORT : CONST.EMPTY_STATE_BACKGROUND;
if (isSmallScreenWidth) {
return {
- minHeight: CONST.EMPTY_STATE_BACKGROUND.SMALL_SCREEN.CONTAINER_MINHEIGHT,
+ minHeight: emptyStateBackground.SMALL_SCREEN.CONTAINER_MINHEIGHT,
display: 'flex',
justifyContent: 'space-between',
};
}
return {
- minHeight: CONST.EMPTY_STATE_BACKGROUND.WIDE_SCREEN.CONTAINER_MINHEIGHT,
+ minHeight: emptyStateBackground.WIDE_SCREEN.CONTAINER_MINHEIGHT,
display: 'flex',
justifyContent: 'space-between',
};
diff --git a/src/styles/themes/default.ts b/src/styles/themes/default.ts
index f8be30a9d881..623f4e543fee 100644
--- a/src/styles/themes/default.ts
+++ b/src/styles/themes/default.ts
@@ -89,6 +89,7 @@ const darkTheme = {
[SCREENS.SAVE_THE_WORLD.ROOT]: colors.tangerine800,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
+ [SCREENS.SETTINGS.WALLET]: colors.darkAppBackground,
[SCREENS.SETTINGS.SECURITY]: colors.ice500,
[SCREENS.SETTINGS.STATUS]: colors.green700,
[SCREENS.SETTINGS.ROOT]: colors.darkHighlightBackground,
diff --git a/src/styles/themes/light.ts b/src/styles/themes/light.ts
index 01ceb6cf8c77..44d8706e25b7 100644
--- a/src/styles/themes/light.ts
+++ b/src/styles/themes/light.ts
@@ -89,6 +89,7 @@ const lightTheme = {
[SCREENS.SAVE_THE_WORLD.ROOT]: colors.tangerine800,
[SCREENS.SETTINGS.PREFERENCES]: colors.blue500,
[SCREENS.SETTINGS.WORKSPACES]: colors.pink800,
+ [SCREENS.SETTINGS.WALLET]: colors.darkAppBackground,
[SCREENS.SETTINGS.SECURITY]: colors.ice500,
[SCREENS.SETTINGS.STATUS]: colors.green700,
[SCREENS.SETTINGS.ROOT]: colors.lightHighlightBackground,
diff --git a/tests/perf-test/ReportActionsList.perf-test.js b/tests/perf-test/ReportActionsList.perf-test.js
new file mode 100644
index 000000000000..4d83cc435636
--- /dev/null
+++ b/tests/perf-test/ReportActionsList.perf-test.js
@@ -0,0 +1,188 @@
+import {measurePerformance} from 'reassure';
+import Onyx from 'react-native-onyx';
+import {screen, fireEvent} from '@testing-library/react-native';
+import ReportActionsList from '../../src/pages/home/report/ReportActionsList';
+import ComposeProviders from '../../src/components/ComposeProviders';
+import OnyxProvider from '../../src/components/OnyxProvider';
+import {ReportAttachmentsProvider} from '../../src/pages/home/report/ReportAttachmentsContext';
+import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions';
+import {LocaleContextProvider} from '../../src/components/LocaleContextProvider';
+import * as LHNTestUtils from '../utils/LHNTestUtils';
+import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
+import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
+import PusherHelper from '../utils/PusherHelper';
+import variables from '../../src/styles/variables';
+import {ActionListContext, ReactionListContext} from '../../src/pages/home/ReportScreenContext';
+import ONYXKEYS from '../../src/ONYXKEYS';
+import * as Localize from '../../src/libs/Localize';
+
+jest.setTimeout(60000);
+
+const mockedNavigate = jest.fn();
+
+jest.mock('../../src/components/withNavigationFocus', () => (Component) => (props) => (
+
+));
+
+jest.mock('@react-navigation/native', () => {
+ const actualNav = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualNav,
+ useRoute: () => mockedNavigate,
+ };
+});
+
+beforeAll(() =>
+ Onyx.init({
+ keys: ONYXKEYS,
+ safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
+ registerStorageEventListener: () => {},
+ }),
+);
+
+afterAll(() => {
+ jest.clearAllMocks();
+});
+
+const mockOnLayout = jest.fn();
+const mockOnScroll = jest.fn();
+const mockLoadMoreChats = jest.fn();
+const mockRef = {current: null};
+
+// Initialize the network key for OfflineWithFeedback
+beforeEach(() => {
+ PusherHelper.setup();
+ wrapOnyxWithWaitForBatchedUpdates(Onyx);
+ return Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false});
+});
+
+// Clear out Onyx after each test so that each test starts with a clean slate
+afterEach(() => {
+ Onyx.clear();
+ PusherHelper.teardown();
+});
+
+const getFakeReportAction = (index) => ({
+ actionName: 'ADDCOMMENT',
+ actorAccountID: index,
+ automatic: false,
+ avatar: '',
+ created: '2023-09-12 16:27:35.124',
+ isAttachment: true,
+ isFirstItem: false,
+ lastModified: '2021-07-14T15:00:00Z',
+ message: [
+ {
+ html: 'hey',
+ isDelatedParentAction: false,
+ isEdited: false,
+ reactions: [],
+ text: 'test',
+ type: 'TEXT',
+ whisperedTo: [],
+ },
+ ],
+ originalMessage: {
+ html: 'hey',
+ lastModified: '2021-07-14T15:00:00Z',
+ },
+ pendingAction: null,
+ person: [
+ {
+ type: 'TEXT',
+ style: 'strong',
+ text: 'email@test.com',
+ },
+ ],
+ previousReportActionID: '1',
+ reportActionID: index.toString(),
+ reportActionTimestamp: 1696243169753,
+ sequenceNumber: 2,
+ shouldShow: true,
+ timestamp: 1696243169,
+ whisperedToAccountIDs: [],
+});
+
+const getMockedSortedReportActions = (length = 100) => Array.from({length}, (__, i) => getFakeReportAction(i));
+
+const currentUserAccountID = 5;
+
+function ReportActionsListWrapper() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+test('should render ReportActionsList with 500 reportActions stored', () => {
+ const scenario = async () => {
+ await screen.findByTestId('report-actions-list');
+ const hintText = Localize.translateLocal('accessibilityHints.chatMessage');
+ // Ensure that the list of items is rendered
+ await screen.findAllByLabelText(hintText);
+ };
+
+ return waitForBatchedUpdates()
+ .then(() =>
+ Onyx.multiSet({
+ [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails,
+ }),
+ )
+ .then(() => measurePerformance(, {scenario}));
+});
+
+test('should scroll and click some of the reports', () => {
+ const eventData = {
+ nativeEvent: {
+ contentOffset: {
+ y: variables.optionRowHeight * 5,
+ },
+ contentSize: {
+ // Dimensions of the scrollable content
+ height: variables.optionRowHeight * 10,
+ width: 100,
+ },
+ layoutMeasurement: {
+ // Dimensions of the device
+ height: variables.optionRowHeight * 5,
+ width: 100,
+ },
+ },
+ };
+
+ const scenario = async () => {
+ const reportActionsList = await screen.findByTestId('report-actions-list');
+ expect(reportActionsList).toBeDefined();
+
+ fireEvent.scroll(reportActionsList, eventData);
+
+ const hintText = Localize.translateLocal('accessibilityHints.chatMessage');
+ const reportItems = await screen.findAllByLabelText(hintText);
+
+ fireEvent.press(reportItems[0], 'onLongPress');
+ };
+
+ return waitForBatchedUpdates()
+ .then(() =>
+ Onyx.multiSet({
+ [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails,
+ }),
+ )
+ .then(() => measurePerformance(, {scenario}));
+});