diff --git a/.github/scripts/createHelpRedirects.sh b/.github/scripts/createHelpRedirects.sh
index 1ae2220253c4..14ed9de953fc 100755
--- a/.github/scripts/createHelpRedirects.sh
+++ b/.github/scripts/createHelpRedirects.sh
@@ -19,7 +19,7 @@ function checkCloudflareResult {
if ! [[ "$RESULT_MESSAGE" == "true" ]]; then
ERROR_MESSAGE=$(echo "$RESULTS" | jq .errors)
- error "Error calling Cloudfalre API: $ERROR_MESSAGE"
+ error "Error calling Cloudflare API: $ERROR_MESSAGE"
exit 1
fi
}
diff --git a/__mocks__/@react-navigation/native/index.js b/__mocks__/@react-navigation/native/index.ts
similarity index 67%
rename from __mocks__/@react-navigation/native/index.js
rename to __mocks__/@react-navigation/native/index.ts
index 09abd0d02bf9..aa8067a1c862 100644
--- a/__mocks__/@react-navigation/native/index.js
+++ b/__mocks__/@react-navigation/native/index.ts
@@ -1,7 +1,7 @@
import {useIsFocused as realUseIsFocused} from '@react-navigation/native';
// We only want this mocked for storybook, not jest
-const useIsFocused = process.env.NODE_ENV === 'test' ? realUseIsFocused : () => true;
+const useIsFocused: typeof realUseIsFocused = process.env.NODE_ENV === 'test' ? realUseIsFocused : () => true;
export * from '@react-navigation/core';
export * from '@react-navigation/native';
diff --git a/android/app/build.gradle b/android/app/build.gradle
index e179b59f4b30..1301f18d7e8f 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001043702
- versionName "1.4.37-2"
+ versionCode 1001043707
+ versionName "1.4.37-7"
}
flavorDimensions "default"
diff --git a/docs/_data/_routes.yml b/docs/_data/_routes.yml
index 33ef4ebcf0a8..d355e53d8a52 100644
--- a/docs/_data/_routes.yml
+++ b/docs/_data/_routes.yml
@@ -19,8 +19,8 @@ platforms:
icon: /assets/images/accounting.svg
description: From setting up your account to ensuring you get the most out of Expensify’s suite of features, click here to get started on streamlining your expense management journey.
- - href: account-settings
- title: Account Settings
+ - href: settings
+ title: Settings
icon: /assets/images/gears.svg
description: Discover how to personalize your profile, add secondary logins, and grant delegated access to employees with our comprehensive guide on Account Settings.
diff --git a/docs/articles/expensify-classic/account-settings/Account-Details.md b/docs/articles/expensify-classic/settings/Account-Details.md
similarity index 100%
rename from docs/articles/expensify-classic/account-settings/Account-Details.md
rename to docs/articles/expensify-classic/settings/Account-Details.md
diff --git a/docs/articles/expensify-classic/account-settings/Close-Account.md b/docs/articles/expensify-classic/settings/Close-Account.md
similarity index 100%
rename from docs/articles/expensify-classic/account-settings/Close-Account.md
rename to docs/articles/expensify-classic/settings/Close-Account.md
diff --git a/docs/articles/expensify-classic/account-settings/Copilot.md b/docs/articles/expensify-classic/settings/Copilot.md
similarity index 100%
rename from docs/articles/expensify-classic/account-settings/Copilot.md
rename to docs/articles/expensify-classic/settings/Copilot.md
diff --git a/docs/articles/expensify-classic/account-settings/Merge-Accounts.md b/docs/articles/expensify-classic/settings/Merge-Accounts.md
similarity index 100%
rename from docs/articles/expensify-classic/account-settings/Merge-Accounts.md
rename to docs/articles/expensify-classic/settings/Merge-Accounts.md
diff --git a/docs/articles/expensify-classic/account-settings/Notification-Troubleshooting.md b/docs/articles/expensify-classic/settings/Notification-Troubleshooting.md
similarity index 100%
rename from docs/articles/expensify-classic/account-settings/Notification-Troubleshooting.md
rename to docs/articles/expensify-classic/settings/Notification-Troubleshooting.md
diff --git a/docs/articles/expensify-classic/account-settings/Preferences.md b/docs/articles/expensify-classic/settings/Preferences.md
similarity index 100%
rename from docs/articles/expensify-classic/account-settings/Preferences.md
rename to docs/articles/expensify-classic/settings/Preferences.md
diff --git a/docs/expensify-classic/hubs/account-settings/index.html b/docs/expensify-classic/hubs/settings/index.html
similarity index 100%
rename from docs/expensify-classic/hubs/account-settings/index.html
rename to docs/expensify-classic/hubs/settings/index.html
diff --git a/docs/redirects.csv b/docs/redirects.csv
index 988b07d729f0..2609f6665c8d 100644
--- a/docs/redirects.csv
+++ b/docs/redirects.csv
@@ -34,8 +34,8 @@ https://help.expensify.com/articles/expensify-classic/expensify-card/Expensify-C
https://help.expensify.com/articles/expensify-classic/expensify-partner-program/How-to-Join-the-ExpensifyApproved!-Partner-Program.html,https://use.expensify.com/accountants-program
https://help.expensify.com/articles/expensify-classic/getting-started/approved-accountants/Card-Revenue-Share-For-Expensify-Approved-Partners, https://use.expensify.com/blog/maximizing-rewards-expensifyapproved-accounting-partners-now-earn-0-5-revenue-share
https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/International-Reimbursements,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Global-Reimbursements
-https://community.expensify.com/discussion/4452/how-to-merge-accounts,https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts#gsc.tab=0
-https://community.expensify.com/discussion/4783/how-to-add-or-remove-a-copilot#latest,https://help.expensify.com/articles/expensify-classic/account-settings/Copilot#gsc.tab=0
+https://community.expensify.com/discussion/4452/how-to-merge-accounts,https://help.expensify.com/articles/expensify-classic/account-settings/Merge-Accounts
+https://community.expensify.com/discussion/4783/how-to-add-or-remove-a-copilot,https://help.expensify.com/articles/expensify-classic/account-settings/Copilot
https://community.expensify.com/discussion/4343/expensify-anz-partnership-announcement,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Connect-ANZ
https://community.expensify.com/discussion/7318/deep-dive-company-credit-card-import-options,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards
https://community.expensify.com/discussion/2673/personalize-your-commercial-card-feed-name,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/company-cards/Commercial-Card-Feeds
@@ -48,5 +48,5 @@ https://community.expensify.com/discussion/4463/how-to-remove-or-manage-settings
https://community.expensify.com/discussion/5793/how-to-connect-your-personal-card-to-import-expenses,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-credit-cards/Personal-Credit-Cards
https://community.expensify.com/discussion/4826/how-to-set-your-annual-subscription-size,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription
https://community.expensify.com/discussion/5667/deep-dive-how-does-the-annual-subscription-billing-work,https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Annual-Subscription
-https://help.expensify.com/expensify-classic/hubs/getting-started/plan-types,https://www.expensify.com/pricing
+https://help.expensify.com/expensify-classic/hubs/getting-started/plan-types,https://use.expensify.com/
https://help.expensify.com/articles/expensify-classic/getting-started/Employees,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index b529bbbcd100..a2effb8de0b1 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.37.2
+ 1.4.37.7
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index af5704061df2..02b01e7153d0 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.37.2
+ 1.4.37.7
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 8209491d19ff..69e1e7d6e9d9 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.37
CFBundleVersion
- 1.4.37.2
+ 1.4.37.7
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 072684aa77c2..11099886bfb0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.37-2",
+ "version": "1.4.37-7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.37-2",
+ "version": "1.4.37-7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index e08a8bdcb70a..71983e0e1679 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.37-2",
+ "version": "1.4.37-7",
"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/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js
index 3646d9148b3a..cd6ada90b58b 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js
@@ -60,7 +60,7 @@ function MentionUserRenderer(props) {
if (!_.isEmpty(htmlAttributeAccountID)) {
const user = lodashGet(personalDetails, htmlAttributeAccountID);
accountID = parseInt(htmlAttributeAccountID, 10);
- displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(lodashGet(user, 'login', '')) || lodashGet(user, 'displayName', '') || translate('common.hidden');
+ displayNameOrLogin = lodashGet(user, 'displayName', '') || LocalePhoneNumber.formatPhoneNumber(lodashGet(user, 'login', '')) || translate('common.hidden');
displayNameOrLogin = getMentionDisplayText(displayNameOrLogin, htmlAttributeAccountID, lodashGet(user, 'login', ''));
navigationRoute = ROUTES.PROFILE.getRoute(htmlAttributeAccountID);
} else if (!_.isEmpty(tnode.data)) {
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 0bf649b54a6e..4d7089fb24bd 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -384,7 +384,11 @@ function MenuItem(
onPress(event);
return;
}
- singleExecution(waitForNavigate(() => onPress(event)))();
+ singleExecution(
+ waitForNavigate(() => {
+ onPress(event);
+ }),
+ )();
}
};
diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx
index f1c7539cc6b5..4e0ed1f573f9 100644
--- a/src/components/ReferralProgramCTA.tsx
+++ b/src/components/ReferralProgramCTA.tsx
@@ -1,32 +1,45 @@
import React from 'react';
+import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as User from '@userActions/User';
import CONST from '@src/CONST';
import Navigation from '@src/libs/Navigation/Navigation';
+import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {DismissedReferralBanners} from '@src/types/onyx/Account';
import Icon from './Icon';
import {Close} from './Icon/Expensicons';
import {PressableWithoutFeedback} from './Pressable';
import Text from './Text';
import Tooltip from './Tooltip';
-type ReferralProgramCTAProps = {
+type ReferralProgramCTAOnyxProps = {
+ dismissedReferralBanners: DismissedReferralBanners;
+};
+
+type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & {
referralContentType:
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY
| typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND;
-
- /** Method to trigger when pressing close button of the banner */
- onCloseButtonPress?: () => void;
};
-function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}}: ReferralProgramCTAProps) {
+function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: ReferralProgramCTAProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();
+ const handleDismissCallToAction = () => {
+ User.dismissReferralBanner(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND);
+ };
+
+ if (!referralContentType || dismissedReferralBanners[referralContentType]) {
+ return null;
+ }
+
return (
{
@@ -47,7 +60,7 @@ function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}}
{
e.preventDefault();
}}
@@ -67,4 +80,9 @@ function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}}
);
}
-export default ReferralProgramCTA;
+export default withOnyx({
+ dismissedReferralBanners: {
+ key: ONYXKEYS.ACCOUNT,
+ selector: (data) => data?.dismissedReferralBanners ?? {},
+ },
+})(ReferralProgramCTA);
diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx
index ff29bf5b0ee8..9e169b23391a 100644
--- a/src/components/ReportActionItem/MoneyRequestAction.tsx
+++ b/src/components/ReportActionItem/MoneyRequestAction.tsx
@@ -92,7 +92,7 @@ function MoneyRequestAction({
}
// If the childReportID is not present, we need to create a new thread
- const childReportID = action?.childReportID ?? '0';
+ const childReportID = action?.childReportID;
if (!childReportID) {
const thread = ReportUtils.buildTransactionThread(action, requestReportID);
const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []);
diff --git a/src/components/ReportActionItem/MoneyRequestPreview.tsx b/src/components/ReportActionItem/MoneyRequestPreview.tsx
index f321c63375d0..70a313c77e9e 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview.tsx
@@ -321,7 +321,7 @@ function MoneyRequestPreview({
{shouldShowDescription && }
{shouldShowMerchant && {merchantOrDescription}}
- {isBillSplit && participantAccountIDs.length > 0 && requestAmount && requestAmount > 0 && (
+ {isBillSplit && participantAccountIDs.length > 0 && !!requestAmount && requestAmount > 0 && (
{translate('iou.amountEach', {
amount: CurrencyUtils.convertToDisplayString(
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index c572e9d5f896..98116a089d73 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -203,7 +203,7 @@ function ReportPreview({
if (isApproved) {
return translate('iou.managerApproved', {manager: payerOrApproverName});
}
- const managerName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
+ const managerName = isPolicyExpenseChat && !hasNonReimbursableTransactions ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true);
let paymentVerb: TranslationPaths = hasNonReimbursableTransactions ? 'iou.payerSpent' : 'iou.payerOwes';
if (iouSettled || iouReport?.isWaitingOnBankAccount) {
paymentVerb = 'iou.payerPaid';
diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx
index 5fc75d423a67..e23e1354652b 100644
--- a/src/components/ReportActionItem/TaskPreview.tsx
+++ b/src/components/ReportActionItem/TaskPreview.tsx
@@ -6,7 +6,6 @@ import type {OnyxEntry} from 'react-native-onyx';
import Checkbox from '@components/Checkbox';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
-import {usePersonalDetails} from '@components/OnyxProvider';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import RenderHTML from '@components/RenderHTML';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
@@ -18,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import getButtonState from '@libs/getButtonState';
-import * as LocalePhoneNumber from '@libs/LocalePhoneNumber';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import * as TaskUtils from '@libs/TaskUtils';
@@ -65,7 +63,6 @@ type TaskPreviewProps = WithCurrentUserPersonalDetailsProps &
function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatReportID, checkIfContextMenuActive, currentUserPersonalDetails, isHovered = false}: TaskPreviewProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT;
const {translate} = useLocalize();
// The reportAction might not contain details regarding the taskReport
@@ -76,13 +73,8 @@ function TaskPreview({taskReport, taskReportID, action, contextMenuAnchor, chatR
: action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED;
const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? ''));
const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? '';
- const assigneeLogin = personalDetails[taskAssigneeAccountID]?.login ?? '';
- const assigneeDisplayName = personalDetails[taskAssigneeAccountID]?.displayName ?? '';
- const taskAssignee = assigneeDisplayName || LocalePhoneNumber.formatPhoneNumber(assigneeLogin);
const htmlForTaskPreview =
- taskAssignee && taskAssigneeAccountID !== 0
- ? `@${taskAssignee} ${taskTitle}`
- : `${taskTitle}`;
+ taskAssigneeAccountID !== 0 ? ` ${taskTitle}` : `${taskTitle}`;
const isDeletedParentAction = ReportUtils.isCanceledTaskReport(taskReport, action);
if (isDeletedParentAction) {
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index aa5b15973b9f..83ce2145fcdb 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -58,6 +58,7 @@ function BaseSelectionList(
shouldShowTooltips = true,
shouldUseDynamicMaxToRenderPerBatch = false,
rightHandSideComponent,
+ isLoadingNewOptions = false,
}: BaseSelectionListProps,
inputRef: ForwardedRef,
) {
@@ -412,6 +413,7 @@ function BaseSelectionList(
spellCheck={false}
onSubmitEditing={selectFocusedOption}
blurOnSubmit={!!flattenedSections.allOptions.length}
+ isLoading={isLoadingNewOptions}
/>
)}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index a82ddef6febb..222c818dd66d 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -230,6 +230,9 @@ type BaseSelectionListProps = Partial ReactElement) | ReactElement | null;
+
+ /** Whether to show the loading indicator for new options */
+ isLoadingNewOptions?: boolean;
};
type ItemLayout = {
diff --git a/src/components/ValuePicker/ValueSelectorModal.js b/src/components/ValuePicker/ValueSelectorModal.js
index e45ba873d8a3..f93df86c9ab9 100644
--- a/src/components/ValuePicker/ValueSelectorModal.js
+++ b/src/components/ValuePicker/ValueSelectorModal.js
@@ -1,6 +1,6 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
-import React, {useEffect, useState} from 'react';
+import React, {useMemo} from 'react';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import ScreenWrapper from '@components/ScreenWrapper';
@@ -42,11 +42,9 @@ const defaultProps = {
function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onItemSelected, shouldShowTooltips}) {
const styles = useThemeStyles();
- const [sectionsData, setSectionsData] = useState([]);
-
- useEffect(() => {
+ const sections = useMemo(() => {
const itemsData = _.map(items, (item) => ({value: item.value, alternateText: item.description, keyForList: item.value, text: item.label, isSelected: item === selectedItem}));
- setSectionsData(itemsData);
+ return [{data: itemsData}];
}, [items, selectedItem]);
return (
@@ -69,7 +67,7 @@ function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onI
onBackButtonPress={onClose}
/>
, policy: OnyxEntry<
const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID));
- const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? '';
+ const payerOrApproverName =
+ isExpenseReport(report) && !hasNonReimbursableTransactions(report?.reportID ?? '') ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? '';
const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', {
payer: payerOrApproverName,
amount: formattedAmount,
@@ -2244,9 +2245,10 @@ function getReportPreviewMessage(
}
}
+ const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID);
const totalAmount = getMoneyRequestSpendBreakdown(report).totalDisplaySpend;
const policyName = getPolicyName(report, false, policy);
- const payerName = isExpenseReport(report) ? policyName : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport);
+ const payerName = isExpenseReport(report) && !containsNonReimbursable ? policyName : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency);
@@ -2310,8 +2312,6 @@ function getReportPreviewMessage(
return `${requestorName ? `${requestorName}: ` : ''}${Localize.translateLocal('iou.requestedAmount', {formattedAmount: amountToDisplay})}`;
}
- const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID);
-
return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount});
}
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 1bd090430800..dd118c36a8a1 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -3075,7 +3075,7 @@ function getSendMoneyParams(
paymentMethodType,
transactionID: optimisticTransaction.transactionID,
newIOUReportDetails,
- createdReportActionID: isNewChat ? optimisticCreatedAction.reportActionID : '',
+ createdReportActionID: isNewChat ? optimisticCreatedAction.reportActionID : '0',
reportPreviewReportActionID: reportPreviewAction.reportActionID,
},
optimisticData,
diff --git a/src/libs/shouldShowSubscriptionsMenu/index.native.ts b/src/libs/shouldShowSubscriptionsMenu/index.native.ts
new file mode 100644
index 000000000000..c98302e9a87d
--- /dev/null
+++ b/src/libs/shouldShowSubscriptionsMenu/index.native.ts
@@ -0,0 +1,8 @@
+import type ShouldShowSubscriptionsMenu from './types';
+
+/**
+ * Indicates whether the subscription menu should show in the all settings screen
+ */
+const shouldShowSubscriptionsMenu: ShouldShowSubscriptionsMenu = false;
+
+export default shouldShowSubscriptionsMenu;
diff --git a/src/libs/shouldShowSubscriptionsMenu/index.ts b/src/libs/shouldShowSubscriptionsMenu/index.ts
new file mode 100644
index 000000000000..2f2b7f17c2c5
--- /dev/null
+++ b/src/libs/shouldShowSubscriptionsMenu/index.ts
@@ -0,0 +1,8 @@
+import type ShouldShowSubscriptionsMenu from './types';
+
+/**
+ * Indicates whether the subscription menu should show in the all settings screen
+ */
+const shouldShowSubscriptionsMenu: ShouldShowSubscriptionsMenu = true;
+
+export default shouldShowSubscriptionsMenu;
diff --git a/src/libs/shouldShowSubscriptionsMenu/types.tsx b/src/libs/shouldShowSubscriptionsMenu/types.tsx
new file mode 100644
index 000000000000..e72b55234639
--- /dev/null
+++ b/src/libs/shouldShowSubscriptionsMenu/types.tsx
@@ -0,0 +1,3 @@
+type ShouldShowSubscriptionsMenu = boolean;
+
+export default ShouldShowSubscriptionsMenu;
diff --git a/src/pages/ReimbursementAccount/ValidationStep.js b/src/pages/ReimbursementAccount/ValidationStep.js
index 1898fc8ad212..6451cdb2a102 100644
--- a/src/pages/ReimbursementAccount/ValidationStep.js
+++ b/src/pages/ReimbursementAccount/ValidationStep.js
@@ -20,8 +20,11 @@ import TextLink from '@components/TextLink';
import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as CurrencyUtils from '@libs/CurrencyUtils';
+import getPermittedDecimalSeparator from '@libs/getPermittedDecimalSeparator';
import BankAccount from '@libs/models/BankAccount';
import * as ValidationUtils from '@libs/ValidationUtils';
+import withPolicy from '@pages/workspace/withPolicy';
import WorkspaceResetBankAccountModal from '@pages/workspace/WorkspaceResetBankAccountModal';
import * as BankAccounts from '@userActions/BankAccounts';
import * as Report from '@userActions/Report';
@@ -61,23 +64,20 @@ const defaultProps = {
* Any dollar amount (e.g. 1.12) will be returned as 112
*
* @param {String} amount field input
+ * @param {RegExp} amountRegex
* @returns {String}
*/
-const filterInput = (amount) => {
+const filterInput = (amount, amountRegex) => {
let value = amount ? amount.toString().trim() : '';
- if (value === '' || _.isNaN(Number(value)) || !Math.abs(Str.fromUSDToNumber(value))) {
+ value = value.replace(/^0+|0+$/g, '');
+ if (value === '' || _.isNaN(Number(value)) || !Math.abs(Str.fromUSDToNumber(value)) || (amountRegex && !amountRegex.test(value))) {
return '';
}
- // If the user enters the values in dollars, convert it to the respective cents amount
- if (_.contains(value, '.')) {
- value = Str.fromUSDToNumber(value);
- }
-
return value;
};
-function ValidationStep({reimbursementAccount, translate, onBackButtonPress, account, policyID}) {
+function ValidationStep({reimbursementAccount, translate, onBackButtonPress, account, policyID, toLocaleDigit, policy}) {
const styles = useThemeStyles();
/**
* @param {Object} values - form input values passed by the Form component
@@ -85,9 +85,13 @@ function ValidationStep({reimbursementAccount, translate, onBackButtonPress, acc
*/
const validate = (values) => {
const errors = {};
+ const decimalSeparator = toLocaleDigit('.');
+ const outputCurrency = lodashGet(policy, 'outputCurrency', CONST.CURRENCY.USD);
+
+ const amountRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{0,${CurrencyUtils.getCurrencyDecimals(outputCurrency)}})?$`, 'i');
_.each(values, (value, key) => {
- const filteredValue = typeof value === 'string' ? filterInput(value) : value;
+ const filteredValue = typeof value === 'string' ? filterInput(value, amountRegex) : value;
if (ValidationUtils.isRequiredFulfilled(filteredValue)) {
return;
}
@@ -231,6 +235,7 @@ ValidationStep.displayName = 'ValidationStep';
export default compose(
withLocalize,
+ withPolicy,
withOnyx({
account: {
key: ONYXKEYS.ACCOUNT,
diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js
deleted file mode 100755
index d8eef6f447ae..000000000000
--- a/src/pages/SearchPage.js
+++ /dev/null
@@ -1,227 +0,0 @@
-import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useRef, useState} from 'react';
-import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import OptionsSelector from '@components/OptionsSelector';
-import ScreenWrapper from '@components/ScreenWrapper';
-import useLocalize from '@hooks/useLocalize';
-import useNetwork from '@hooks/useNetwork';
-import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
-import * as OptionsListUtils from '@libs/OptionsListUtils';
-import Performance from '@libs/Performance';
-import * as ReportUtils from '@libs/ReportUtils';
-import * as Report from '@userActions/Report';
-import Timing from '@userActions/Timing';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import personalDetailsPropType from './personalDetailsPropType';
-import reportPropTypes from './reportPropTypes';
-
-const propTypes = {
- /* Onyx Props */
-
- /** Beta features list */
- betas: PropTypes.arrayOf(PropTypes.string),
-
- /** All of the personal details for everyone */
- personalDetails: PropTypes.objectOf(personalDetailsPropType),
-
- /** All reports shared with the user */
- reports: PropTypes.objectOf(reportPropTypes),
-
- /** Whether we are searching for reports in the server */
- isSearchingForReports: PropTypes.bool,
-
- /**
- * The navigation prop passed by the navigator.
- *
- * This is required because transitionEnd event doesn't trigger in the automated testing environment.
- */
- navigation: PropTypes.shape({}),
-};
-
-const defaultProps = {
- betas: [],
- personalDetails: {},
- reports: {},
- isSearchingForReports: false,
- navigation: {},
-};
-
-function SearchPage({betas, personalDetails, reports, isSearchingForReports, navigation}) {
- const [searchValue, setSearchValue] = useState('');
- const [searchOptions, setSearchOptions] = useState({
- recentReports: {},
- personalDetails: {},
- userToInvite: {},
- });
-
- const {isOffline} = useNetwork();
- const {translate} = useLocalize();
- const themeStyles = useThemeStyles();
- const isMounted = useRef(false);
-
- const updateOptions = useCallback(() => {
- const {
- recentReports: localRecentReports,
- personalDetails: localPersonalDetails,
- userToInvite: localUserToInvite,
- } = OptionsListUtils.getSearchOptions(reports, personalDetails, searchValue.trim(), betas);
-
- setSearchOptions({
- recentReports: localRecentReports,
- personalDetails: localPersonalDetails,
- userToInvite: localUserToInvite,
- });
- }, [reports, personalDetails, searchValue, betas]);
-
- useEffect(() => {
- Timing.start(CONST.TIMING.SEARCH_RENDER);
- Performance.markStart(CONST.TIMING.SEARCH_RENDER);
- }, []);
-
- useEffect(() => {
- updateOptions();
- }, [reports, personalDetails, betas, updateOptions]);
-
- useEffect(() => {
- if (!isMounted.current) {
- isMounted.current = true;
- return;
- }
-
- updateOptions();
- // Ignoring the rule intentionally, we want to run the code only when search Value changes to prevent additional runs.
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchValue]);
-
- /**
- * Returns the sections needed for the OptionsSelector
- *
- * @returns {Array}
- */
- const getSections = () => {
- const sections = [];
- let indexOffset = 0;
-
- if (searchOptions.recentReports.length > 0) {
- sections.push({
- data: searchOptions.recentReports,
- shouldShow: true,
- indexOffset,
- });
- indexOffset += searchOptions.recentReports.length;
- }
-
- if (searchOptions.personalDetails.length > 0) {
- sections.push({
- data: searchOptions.personalDetails,
- shouldShow: true,
- indexOffset,
- });
- indexOffset += searchOptions.recentReports.length;
- }
-
- if (searchOptions.userToInvite) {
- sections.push({
- data: [searchOptions.userToInvite],
- shouldShow: true,
- indexOffset,
- });
- }
-
- return sections;
- };
-
- const searchRendered = () => {
- Timing.end(CONST.TIMING.SEARCH_RENDER);
- Performance.markEnd(CONST.TIMING.SEARCH_RENDER);
- };
-
- const onChangeText = (value = '') => {
- Report.searchInServer(searchValue);
- setSearchValue(value);
- };
-
- /**
- * Reset the search value and redirect to the selected report
- *
- * @param {Object} option
- */
- const selectReport = (option) => {
- if (!option) {
- return;
- }
- if (option.reportID) {
- Navigation.dismissModal(option.reportID);
- } else {
- Report.navigateToAndOpenReport([option.login]);
- }
- };
-
- const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails);
- const headerMessage = OptionsListUtils.getHeaderMessage(
- searchOptions.recentReports.length + searchOptions.personalDetails.length !== 0,
- Boolean(searchOptions.userToInvite),
- searchValue,
- );
-
- return (
-
- {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
- <>
-
-
-
-
- >
- )}
-
- );
-}
-
-SearchPage.propTypes = propTypes;
-SearchPage.defaultProps = defaultProps;
-SearchPage.displayName = 'SearchPage';
-export default withOnyx({
- reports: {
- key: ONYXKEYS.COLLECTION.REPORT,
- },
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- betas: {
- key: ONYXKEYS.BETAS,
- },
- isSearchingForReports: {
- key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
- initWithStoredValues: false,
- },
-})(SearchPage);
diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx
index 8e23c658f4aa..3d5ebfd2c193 100644
--- a/src/pages/SearchPage/SearchPageFooter.tsx
+++ b/src/pages/SearchPage/SearchPageFooter.tsx
@@ -1,44 +1,19 @@
-import React, {useState} from 'react';
+import React from 'react';
import {View} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
import ReferralProgramCTA from '@components/ReferralProgramCTA';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as User from '@userActions/User';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {DismissedReferralBanners} from '@src/types/onyx/Account';
-type SearchPageFooterOnyxProps = {
- dismissedReferralBanners: DismissedReferralBanners;
-};
-function SearchPageFooter({dismissedReferralBanners}: SearchPageFooterOnyxProps) {
- const [shouldShowReferralCTA, setShouldShowReferralCTA] = useState(!dismissedReferralBanners[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]);
+function SearchPageFooter() {
const themeStyles = useThemeStyles();
- const closeCallToActionBanner = () => {
- setShouldShowReferralCTA(false);
- User.dismissReferralBanner(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND);
- };
-
return (
- <>
- {shouldShowReferralCTA && (
-
-
-
- )}
- >
+
+
+
);
}
SearchPageFooter.displayName = 'SearchPageFooter';
-export default withOnyx({
- dismissedReferralBanners: {
- key: ONYXKEYS.ACCOUNT,
- selector: (data) => data?.dismissedReferralBanners ?? {},
- },
-})(SearchPageFooter);
+export default SearchPageFooter;
diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js
index 211f3622e06c..8a06d54a1f45 100644
--- a/src/pages/SearchPage/index.js
+++ b/src/pages/SearchPage/index.js
@@ -29,11 +29,15 @@ const propTypes = {
/** All reports shared with the user */
reports: PropTypes.objectOf(reportPropTypes),
+
+ /** Whether or not we are searching for reports on the server */
+ isSearchingForReports: PropTypes.bool,
};
const defaultProps = {
betas: [],
reports: {},
+ isSearchingForReports: false,
};
const setPerformanceTimersEnd = () => {
@@ -43,7 +47,7 @@ const setPerformanceTimersEnd = () => {
const SearchPageFooterInstance = ;
-function SearchPage({betas, reports}) {
+function SearchPage({betas, reports, isSearchingForReports}) {
const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false);
const {translate} = useLocalize();
const {isOffline} = useNetwork();
@@ -59,10 +63,9 @@ function SearchPage({betas, reports}) {
Performance.markStart(CONST.TIMING.SEARCH_RENDER);
}, []);
- const onChangeText = (text = '') => {
- Report.searchInServer(text);
- setSearchValue(text);
- };
+ useEffect(() => {
+ Report.searchInServer(debouncedSearchValue.trim());
+ }, [debouncedSearchValue]);
const {
recentReports,
@@ -150,13 +153,14 @@ function SearchPage({betas, reports}) {
textInputValue={searchValue}
textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')}
textInputHint={offlineMessage}
- onChangeText={onChangeText}
+ onChangeText={setSearchValue}
headerMessage={headerMessage}
onLayout={setPerformanceTimersEnd}
autoFocus
onSelectRow={selectReport}
showLoadingPlaceholder={!didScreenTransitionEnd || !isOptionsDataReady}
footerContent={SearchPageFooterInstance}
+ isLoadingNewOptions={isSearchingForReports}
/>
>
diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx
index 831f0eb8f1d8..dcfb9f6861bf 100644
--- a/src/pages/ShareCodePage.tsx
+++ b/src/pages/ShareCodePage.tsx
@@ -109,7 +109,8 @@ function ShareCodePage({report, session, currentUserPersonalDetails}: ShareCodeP
isAnonymousAction
title={translate('common.download')}
icon={Expensicons.Download}
- onPress={qrCodeRef.current?.download}
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ onPress={() => qrCodeRef.current?.download?.()}
/>
)}
diff --git a/src/pages/home/sidebar/AllSettingsScreen.tsx b/src/pages/home/sidebar/AllSettingsScreen.tsx
index aa308d523db4..0406c38cf659 100644
--- a/src/pages/home/sidebar/AllSettingsScreen.tsx
+++ b/src/pages/home/sidebar/AllSettingsScreen.tsx
@@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Navigation from '@libs/Navigation/Navigation';
+import shouldShowSubscriptionsMenu from '@libs/shouldShowSubscriptionsMenu';
import {hasGlobalWorkspaceSettingsRBR} from '@libs/WorkspacesSettingsUtils';
import * as Link from '@userActions/Link';
import CONST from '@src/CONST';
@@ -49,16 +50,20 @@ function AllSettingsScreen({policies, policyMembers}: AllSettingsScreenProps) {
focused: !isSmallScreenWidth,
brickRoadIndicator: hasGlobalWorkspaceSettingsRBR(policies, policyMembers) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
},
- {
- translationKey: 'allSettingsScreen.subscriptions',
- icon: Expensicons.MoneyBag,
- action: () => {
- Link.openOldDotLink(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL);
- },
- shouldShowRightIcon: true,
- iconRight: Expensicons.NewWindow,
- link: CONST.OLDDOT_URLS.ADMIN_POLICIES_URL,
- },
+ ...(shouldShowSubscriptionsMenu
+ ? [
+ {
+ translationKey: 'allSettingsScreen.subscriptions',
+ icon: Expensicons.MoneyBag,
+ action: () => {
+ Link.openOldDotLink(CONST.OLDDOT_URLS.ADMIN_POLICIES_URL);
+ },
+ shouldShowRightIcon: true,
+ iconRight: Expensicons.NewWindow,
+ link: CONST.OLDDOT_URLS.ADMIN_POLICIES_URL,
+ },
+ ]
+ : []),
{
translationKey: 'allSettingsScreen.cardsAndDomains',
icon: Expensicons.CardsAndDomains,
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js
index be57de687e3a..207aa9ff47b1 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.js
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js
@@ -128,6 +128,18 @@ function IOURequestStepConfirmation({
IOU.setMoneyRequestBillable_temporaryForRefactor(transactionID, defaultBillable);
}, [transactionID, defaultBillable]);
+ const defaultCategory = lodashGet(
+ _.find(lodashGet(policy, 'customUnits', {}), (customUnit) => customUnit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE),
+ 'defaultCategory',
+ '',
+ );
+ useEffect(() => {
+ if (requestType !== CONST.IOU.REQUEST_TYPE.DISTANCE || !_.isEmpty(transaction.category)) {
+ return;
+ }
+ IOU.setMoneyRequestCategory_temporaryForRefactor(transactionID, defaultCategory);
+ }, [transactionID, transaction.category, requestType, defaultCategory]);
+
const navigateBack = useCallback(() => {
// If there is not a report attached to the IOU with a reportID, then the participants were manually selected and the user needs taken
// back to the participants step
@@ -419,7 +431,7 @@ function IOURequestStepConfirmation({
iouMerchant={transaction.merchant}
iouCreated={transaction.created}
isDistanceRequest={requestType === CONST.IOU.REQUEST_TYPE.DISTANCE}
- shouldShowSmartScanFields={_.isEmpty(lodashGet(transaction, 'receipt.source', ''))}
+ shouldShowSmartScanFields={requestType !== CONST.IOU.REQUEST_TYPE.SCAN}
/>
)}
diff --git a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js
index 947252649cc4..649e42bfffbe 100644
--- a/src/pages/settings/Wallet/ActivatePhysicalCardPage.js
+++ b/src/pages/settings/Wallet/ActivatePhysicalCardPage.js
@@ -133,7 +133,7 @@ function ActivatePhysicalCardPage({
return (
Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))}
+ onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))}
backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.PREFERENCES.ROOT].backgroundColor}
illustration={LottieAnimations.Magician}
scrollViewContainerStyles={[styles.mnh100]}