diff --git a/assets/images/document-plus.svg b/assets/images/document-plus.svg
new file mode 100644
index 000000000000..cce2e3027cea
--- /dev/null
+++ b/assets/images/document-plus.svg
@@ -0,0 +1,5 @@
+
diff --git a/ios/NewApp_AdHoc.mobileprovision.gpg b/ios/NewApp_AdHoc.mobileprovision.gpg
index 2c5350cec2aa..c9b8286cf50f 100644
Binary files a/ios/NewApp_AdHoc.mobileprovision.gpg and b/ios/NewApp_AdHoc.mobileprovision.gpg differ
diff --git a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg
index bae3cd9f3e21..18fbfec9390f 100644
Binary files a/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg and b/ios/NewApp_AdHoc_Notification_Service.mobileprovision.gpg differ
diff --git a/src/CONST.ts b/src/CONST.ts
index bb191ac5e028..750f0867b653 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -332,6 +332,7 @@ const CONST = {
BETA_COMMENT_LINKING: 'commentLinking',
VIOLATIONS: 'violations',
REPORT_FIELDS: 'reportFields',
+ TRACK_EXPENSE: 'trackExpense',
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission',
},
@@ -513,7 +514,7 @@ const CONST = {
EUR: 'EUR',
},
get DIRECT_REIMBURSEMENT_CURRENCIES() {
- return [this.CURRENCY.USD, this.CURRENCY.AUD, this.CURRENCY.CAD, this.CURRENCY.GBP, this.CURRENCY.NZD, this.CURRENCY.EUR];
+ return [this.CURRENCY.USD, this.CURRENCY.AUD, this.CURRENCY.CAD, this.CURRENCY.GBP, this.CURRENCY.EUR];
},
EXAMPLE_PHONE_NUMBER: '+15005550006',
CONCIERGE_CHAT_NAME: 'Concierge',
@@ -1344,6 +1345,7 @@ const CONST = {
SEND: 'send',
SPLIT: 'split',
REQUEST: 'request',
+ TRACK_EXPENSE: 'track-expense',
},
REQUEST_TYPE: {
DISTANCE: 'distance',
@@ -1358,6 +1360,7 @@ const CONST = {
CANCEL: 'cancel',
DELETE: 'delete',
APPROVE: 'approve',
+ TRACK: 'track',
},
AMOUNT_MAX_LENGTH: 10,
RECEIPT_STATE: {
@@ -1472,6 +1475,15 @@ const CONST = {
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
},
+ MORE_FEATURES: {
+ ARE_CATEGORIES_ENABLED: 'areCategoriesEnabled',
+ ARE_TAGS_ENABLED: 'areTagsEnabled',
+ ARE_DISTANCE_RATES_ENABLED: 'areDistanceRatesEnabled',
+ ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled',
+ ARE_REPORTFIELDS_ENABLED: 'areReportFieldsEnabled',
+ ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled',
+ ARE_TAXES_ENABLED: 'tax',
+ },
CATEGORIES_BULK_ACTION_TYPES: {
DELETE: 'delete',
DISABLE: 'disable',
@@ -1487,6 +1499,21 @@ const CONST = {
DISABLE: 'disable',
ENABLE: 'enable',
},
+ TAX_RATES_BULK_ACTION_TYPES: {
+ DELETE: 'delete',
+ DISABLE: 'disable',
+ ENABLE: 'enable',
+ },
+ COLLECTION_KEYS: {
+ DESCRIPTION: 'description',
+ REIMBURSER_EMAIL: 'reimburserEmail',
+ REIMBURSEMENT_CHOICE: 'reimbursementChoice',
+ APPROVAL_MODE: 'approvalMode',
+ AUTOREPORTING: 'autoReporting',
+ AUTOREPORTING_FREQUENCY: 'autoReportingFrequency',
+ AUTOREPORTING_OFFSET: 'autoReportingOffset',
+ GENERAL_SETTINGS: 'generalSettings',
+ },
},
CUSTOM_UNITS: {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 99973935b20a..d74e691fe10e 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -16,9 +16,6 @@ const ONYXKEYS = {
/** Holds the reportID for the report between the user and their account manager */
ACCOUNT_MANAGER_REPORT_ID: 'accountManagerReportID',
- /** Boolean flag only true when first set */
- NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'isFirstTimeNewExpensifyUser',
-
/** Holds an array of client IDs which is used for multi-tabs on web in order to know
* which tab is the leader, and which ones are the followers */
ACTIVE_CLIENTS: 'activeClients',
@@ -106,27 +103,52 @@ const ONYXKEYS = {
STASHED_SESSION: 'stashedSession',
BETAS: 'betas',
- /** NVP keys
+ /** NVP keys */
+
+ /** Boolean flag only true when first set */
+ NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser',
+
/** Contains the user preference for the LHN priority mode */
NVP_PRIORITY_MODE: 'nvp_priorityMode',
/** Contains the users's block expiration (if they have one) */
- NVP_BLOCKED_FROM_CONCIERGE: 'private_blockedFromConcierge',
+ NVP_BLOCKED_FROM_CONCIERGE: 'nvp_private_blockedFromConcierge',
/** A unique identifier that each user has that's used to send notifications */
- NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'private_pushNotificationID',
+ NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'nvp_private_pushNotificationID',
/** The NVP with the last payment method used per policy */
- NVP_LAST_PAYMENT_METHOD: 'nvp_lastPaymentMethod',
+ NVP_LAST_PAYMENT_METHOD: 'nvp_private_lastPaymentMethod',
/** This NVP holds to most recent waypoints that a person has used when creating a distance request */
NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints',
/** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */
- NVP_HAS_DISMISSED_IDLE_PANEL: 'hasDismissedIdlePanel',
+ NVP_HAS_DISMISSED_IDLE_PANEL: 'nvp_hasDismissedIdlePanel',
/** This NVP contains the choice that the user made on the engagement modal */
- NVP_INTRO_SELECTED: 'introSelected',
+ NVP_INTRO_SELECTED: 'nvp_introSelected',
+
+ /** This NVP contains the active policyID */
+ NVP_ACTIVE_POLICY_ID: 'nvp_expensify_activePolicyID',
+
+ /** This NVP contains the referral banners the user dismissed */
+ NVP_DISMISSED_REFERRAL_BANNERS: 'nvp_dismissedReferralBanners',
+
+ /** Indicates which locale should be used */
+ NVP_PREFERRED_LOCALE: 'nvp_preferredLocale',
+
+ /** Whether the user has tried focus mode yet */
+ NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode',
+
+ /** Whether the user has been shown the hold educational interstitial yet */
+ NVP_HOLD_USE_EXPLAINED: 'holdUseExplained',
+
+ /** Store preferred skintone for emoji */
+ PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone',
+
+ /** Store frequently used emojis for this user */
+ FREQUENTLY_USED_EMOJIS: 'nvp_expensify_frequentlyUsedEmojis',
/** The NVP with the last distance rate used per policy */
NVP_LAST_SELECTED_DISTANCE_RATES: 'lastSelectedDistanceRates',
@@ -153,9 +175,6 @@ const ONYXKEYS = {
ONFIDO_TOKEN: 'onfidoToken',
ONFIDO_APPLICANT_ID: 'onfidoApplicantID',
- /** Indicates which locale should be used */
- NVP_PREFERRED_LOCALE: 'preferredLocale',
-
/** User's Expensify Wallet */
USER_WALLET: 'userWallet',
@@ -177,12 +196,6 @@ const ONYXKEYS = {
/** The user's cash card and imported cards (including the Expensify Card) */
CARD_LIST: 'cardList',
- /** Whether the user has tried focus mode yet */
- NVP_TRY_FOCUS_MODE: 'tryFocusMode',
-
- /** Whether the user has been shown the hold educational interstitial yet */
- NVP_HOLD_USE_EXPLAINED: 'holdUseExplained',
-
/** Boolean flag used to display the focus mode notification */
FOCUS_MODE_NOTIFICATION: 'focusModeNotification',
@@ -195,12 +208,6 @@ const ONYXKEYS = {
/** Stores information about the active reimbursement account being set up */
REIMBURSEMENT_ACCOUNT: 'reimbursementAccount',
- /** Store preferred skintone for emoji */
- PREFERRED_EMOJI_SKIN_TONE: 'preferredEmojiSkinTone',
-
- /** Store frequently used emojis for this user */
- FREQUENTLY_USED_EMOJIS: 'frequentlyUsedEmojis',
-
/** Stores Workspace ID that will be tied to reimbursement account during setup */
REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID',
@@ -294,8 +301,8 @@ const ONYXKEYS = {
POLICY_CATEGORIES: 'policyCategories_',
POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_',
POLICY_TAGS: 'policyTags_',
- POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_',
- POLICY_REPORT_FIELDS: 'policyReportFields_',
+ POLICY_RECENTLY_USED_TAGS: 'nvp_recentlyUsedTags_',
+ OLD_POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_',
WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_',
WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_',
REPORT: 'report_',
@@ -420,6 +427,10 @@ const ONYXKEYS = {
POLICY_TAG_NAME_FORM_DRAFT: 'policyTagNameFormDraft',
WORKSPACE_NEW_TAX_FORM: 'workspaceNewTaxForm',
WORKSPACE_NEW_TAX_FORM_DRAFT: 'workspaceNewTaxFormDraft',
+ WORKSPACE_TAX_NAME_FORM: 'workspaceTaxNameForm',
+ WORKSPACE_TAX_NAME_FORM_DRAFT: 'workspaceTaxNameFormDraft',
+ WORKSPACE_TAX_VALUE_FORM: 'workspaceTaxValueForm',
+ WORKSPACE_TAX_VALUE_FORM_DRAFT: 'workspaceTaxValueFormDraft',
},
} as const;
@@ -471,6 +482,8 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm;
[ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM]: FormTypes.WorkspaceNewTaxForm;
[ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM]: FormTypes.PolicyCreateDistanceRateForm;
+ [ONYXKEYS.FORMS.WORKSPACE_TAX_NAME_FORM]: FormTypes.WorkspaceTaxNameForm;
+ [ONYXKEYS.FORMS.WORKSPACE_TAX_VALUE_FORM]: FormTypes.WorkspaceTaxValueForm;
};
type OnyxFormDraftValuesMapping = {
@@ -486,7 +499,6 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.POLICY_MEMBERS]: OnyxTypes.PolicyMembers;
[ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS]: OnyxTypes.PolicyMember;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES]: OnyxTypes.RecentlyUsedCategories;
- [ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS]: OnyxTypes.PolicyReportFields;
[ONYXKEYS.COLLECTION.DEPRECATED_POLICY_MEMBER_LIST]: OnyxTypes.PolicyMembers;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT]: OnyxTypes.InvitedEmailsToAccountIDs;
[ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MESSAGE_DRAFT]: string;
@@ -506,6 +518,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations;
[ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags;
+ [ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags;
[ONYXKEYS.COLLECTION.SELECTED_TAB]: string;
[ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string;
[ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep;
@@ -561,6 +574,8 @@ type OnyxValuesMapping = {
[ONYXKEYS.ONFIDO_TOKEN]: string;
[ONYXKEYS.ONFIDO_APPLICANT_ID]: string;
[ONYXKEYS.NVP_PREFERRED_LOCALE]: OnyxTypes.Locale;
+ [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string;
+ [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners;
[ONYXKEYS.USER_WALLET]: OnyxTypes.UserWallet;
[ONYXKEYS.WALLET_ONFIDO]: OnyxTypes.WalletOnfido;
[ONYXKEYS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.WalletAdditionalDetails;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 831921f33122..9ce32835c8d7 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -616,6 +616,18 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/taxes/new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes/new` as const,
},
+ WORKSPACE_TAX_EDIT: {
+ route: 'settings/workspaces/:policyID/tax/:taxID',
+ getRoute: (policyID: string, taxID: string) => `settings/workspaces/${policyID}/tax/${encodeURI(taxID)}` as const,
+ },
+ WORKSPACE_TAX_NAME: {
+ route: 'settings/workspaces/:policyID/tax/:taxID/name',
+ getRoute: (policyID: string, taxID: string) => `settings/workspaces/${policyID}/tax/${encodeURI(taxID)}/name` as const,
+ },
+ WORKSPACE_TAX_VALUE: {
+ route: 'settings/workspaces/:policyID/tax/:taxID/value',
+ getRoute: (policyID: string, taxID: string) => `settings/workspaces/${policyID}/tax/${encodeURI(taxID)}/value` as const,
+ },
WORKSPACE_DISTANCE_RATES: {
route: 'settings/workspaces/:policyID/distance-rates',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index cd7bb934247f..4d4e9ea327c6 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -217,6 +217,9 @@ const SCREENS = {
TAGS_EDIT: 'Tags_Edit',
TAG_EDIT: 'Tag_Edit',
TAXES: 'Workspace_Taxes',
+ TAX_EDIT: 'Workspace_Tax_Edit',
+ TAX_NAME: 'Workspace_Tax_Name',
+ TAX_VALUE: 'Workspace_Tax_Value',
TAXES_SETTINGS: 'Workspace_Taxes_Settings',
TAXES_SETTINGS_CUSTOM_TAX_NAME: 'Workspace_Taxes_Settings_CustomTaxName',
TAXES_SETTINGS_WORKSPACE_CURRENCY_DEFAULT: 'Workspace_Taxes_Settings_WorkspaceCurrency',
diff --git a/src/components/AmountPicker/index.tsx b/src/components/AmountPicker/index.tsx
index 701c75175c02..45e511f24748 100644
--- a/src/components/AmountPicker/index.tsx
+++ b/src/components/AmountPicker/index.tsx
@@ -25,7 +25,8 @@ function AmountPicker({value, description, title, errorText = '', onInputChange,
const updateInput = (updatedValue: string) => {
if (updatedValue !== value) {
- onInputChange?.(updatedValue);
+ // We cast the updatedValue to a number and then back to a string to remove any leading zeros and separating commas
+ onInputChange?.(String(Number(updatedValue)));
}
hidePickerModal();
};
diff --git a/src/components/AvatarSkeleton.tsx b/src/components/AvatarSkeleton.tsx
index a6781448c3ba..273143f76098 100644
--- a/src/components/AvatarSkeleton.tsx
+++ b/src/components/AvatarSkeleton.tsx
@@ -6,12 +6,12 @@ import SkeletonViewContentLoader from './SkeletonViewContentLoader';
function AvatarSkeleton() {
const theme = useTheme();
- const skeletonCircleRadius = variables.componentSizeSmall / 2;
+ const skeletonCircleRadius = variables.sidebarAvatarSize / 2;
return (
diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index 16f31b9c7eba..396c10151fbf 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -60,7 +60,7 @@ function AvatarWithDisplayName({
const title = ReportUtils.getReportName(report);
const subtitle = ReportUtils.getChatRoomSubtitle(report);
const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report);
- const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report);
+ const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report);
const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy);
const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails);
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false);
diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts
index 798369292958..83100788761f 100644
--- a/src/components/ButtonWithDropdownMenu/types.ts
+++ b/src/components/ButtonWithDropdownMenu/types.ts
@@ -12,6 +12,8 @@ type WorkspaceMemberBulkActionType = DeepValueOf;
+type WorkspaceTaxRatesBulkActionType = DeepValueOf;
+
type DropdownOption = {
value: TValueType;
text: string;
@@ -73,4 +75,4 @@ type ButtonWithDropdownMenuProps = {
wrapperStyle?: StyleProp;
};
-export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps};
+export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps, WorkspaceTaxRatesBulkActionType};
diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx
index 2919debe9cb1..dd169576186e 100644
--- a/src/components/CheckboxWithLabel.tsx
+++ b/src/components/CheckboxWithLabel.tsx
@@ -108,3 +108,5 @@ function CheckboxWithLabel(
CheckboxWithLabel.displayName = 'CheckboxWithLabel';
export default React.forwardRef(CheckboxWithLabel);
+
+export type {CheckboxWithLabelProps};
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 73a091815460..7116ba2aab67 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -42,6 +42,7 @@ import Concierge from '@assets/images/concierge.svg';
import Connect from '@assets/images/connect.svg';
import Copy from '@assets/images/copy.svg';
import CreditCard from '@assets/images/creditcard.svg';
+import DocumentPlus from '@assets/images/document-plus.svg';
import DocumentSlash from '@assets/images/document-slash.svg';
import Document from '@assets/images/document.svg';
import DotIndicatorUnfilled from '@assets/images/dot-indicator-unfilled.svg';
@@ -314,4 +315,5 @@ export {
ChatBubbleUnread,
ChatBubbleReply,
Lightbulb,
+ DocumentPlus,
};
diff --git a/src/components/MapView/responder/index.android.ts b/src/components/MapView/responder/index.android.ts
deleted file mode 100644
index a0fce71d8ef5..000000000000
--- a/src/components/MapView/responder/index.android.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import {PanResponder} from 'react-native';
-
-const responder = PanResponder.create({
- onStartShouldSetPanResponder: () => true,
- onPanResponderTerminationRequest: () => false,
-});
-
-export default responder;
diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx
index e70e121569fd..5d3231ca0a41 100644
--- a/src/components/MoneyRequestHeader.tsx
+++ b/src/components/MoneyRequestHeader.tsx
@@ -71,11 +71,15 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
const deleteTransaction = useCallback(() => {
if (parentReportAction) {
const iouTransactionID = parentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : '';
+ if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) {
+ IOU.deleteTrackExpense(parentReport?.reportID ?? '', iouTransactionID, parentReportAction, true);
+ return;
+ }
IOU.deleteMoneyRequest(iouTransactionID, parentReportAction, true);
}
setIsDeleteModalVisible(false);
- }, [parentReportAction, setIsDeleteModalVisible]);
+ }, [parentReport?.reportID, parentReportAction, setIsDeleteModalVisible]);
const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction);
@@ -84,7 +88,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
const canHoldOrUnholdRequest = !isSettled && !isApproved && !isDeletedParentAction;
// If the report supports adding transactions to it, then it also supports deleting transactions from it.
- const canDeleteRequest = isActionOwner && ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) && !isDeletedParentAction;
+ const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(report)) && !isDeletedParentAction;
const changeMoneyRequestStatus = () => {
const iouTransactionID = parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? parentReportAction.originalMessage?.IOUTransactionID ?? '' : '';
@@ -109,7 +113,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction,
if (canHoldOrUnholdRequest) {
const isRequestIOU = parentReport?.type === 'iou';
const isHoldCreator = ReportUtils.isHoldCreator(transaction, report?.reportID) && isRequestIOU;
- const canModifyStatus = isPolicyAdmin || isActionOwner || isApprover;
+ const isTrackExpenseReport = ReportUtils.isTrackExpenseReport(report);
+ const canModifyStatus = !isTrackExpenseReport && (isPolicyAdmin || isActionOwner || isApprover);
if (isOnHold && (isHoldCreator || (!isRequestIOU && canModifyStatus))) {
threeDotsMenuItems.push({
icon: Expensicons.Stopwatch,
diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
index 0d1acc31ecdf..138bfc937926 100755
--- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
+++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js
@@ -251,6 +251,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST;
const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT;
const isTypeSend = iouType === CONST.IOU.TYPE.SEND;
+ const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE;
const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit);
const {unit, rate, currency} = mileageRate;
@@ -381,7 +382,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const splitOrRequestOptions = useMemo(() => {
let text;
- if (isTypeSplit && iouAmount === 0) {
+ if (isTypeTrackExpense) {
+ text = translate('iou.trackExpense');
+ } else if (isTypeSplit && iouAmount === 0) {
text = translate('iou.split');
} else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) {
text = translate('iou.request');
@@ -398,7 +401,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
value: iouType,
},
];
- }, [isTypeSplit, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]);
+ }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]);
const selectedParticipants = useMemo(() => _.filter(pickedParticipants, (participant) => participant.selected), [pickedParticipants]);
const personalDetailsOfPayee = useMemo(() => payeePersonalDetails || currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]);
@@ -446,7 +449,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
} else {
const formattedSelectedParticipants = _.map(selectedParticipants, (participant) => ({
...participant,
- isDisabled: !participant.isPolicyExpenseChat && ReportUtils.isOptimisticPersonalDetail(participant.accountID),
+ isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID),
}));
sections.push({
title: translate('common.to'),
@@ -538,6 +541,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({
const navigateToReportOrUserDetail = (option) => {
const activeRoute = Navigation.getActiveRouteWithoutParams();
+ if (option.isSelfDM) {
+ Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute));
+ return;
+ }
+
if (option.accountID) {
Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute));
} else if (option.reportID) {
diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx
index 7b45fd963fe7..c72cdd1fd898 100644
--- a/src/components/OptionRow.tsx
+++ b/src/components/OptionRow.tsx
@@ -229,7 +229,12 @@ function OptionRow({
numberOfLines={isMultilineSupported ? 2 : 1}
textStyles={displayNameStyle}
shouldUseFullTitle={
- !!option.isChatRoom || !!option.isPolicyExpenseChat || !!option.isMoneyRequestReport || !!option.isThread || !!option.isTaskReport
+ !!option.isChatRoom ||
+ !!option.isPolicyExpenseChat ||
+ !!option.isMoneyRequestReport ||
+ !!option.isThread ||
+ !!option.isTaskReport ||
+ !!option.isSelfDM
}
/>
{option.alternateText ? (
@@ -340,3 +345,5 @@ export default React.memo(
prevProps.option.pendingAction === nextProps.option.pendingAction &&
prevProps.option.customIcon === nextProps.option.customIcon,
);
+
+export type {OptionRowProps};
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 4ee070e19893..44a446b56653 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -215,4 +215,4 @@ function PopoverMenu({
PopoverMenu.displayName = 'PopoverMenu';
export default React.memo(PopoverMenu);
-export type {PopoverMenuItem};
+export type {PopoverMenuItem, PopoverMenuProps};
diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx
index 6db37ce1320a..c93b75bf11ad 100644
--- a/src/components/ReferralProgramCTA.tsx
+++ b/src/components/ReferralProgramCTA.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
@@ -8,7 +9,7 @@ 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 type * as OnyxTypes from '@src/types/onyx';
import Icon from './Icon';
import {Close} from './Icon/Expensicons';
import {PressableWithoutFeedback} from './Pressable';
@@ -16,7 +17,7 @@ import Text from './Text';
import Tooltip from './Tooltip';
type ReferralProgramCTAOnyxProps = {
- dismissedReferralBanners: DismissedReferralBanners;
+ dismissedReferralBanners: OnyxEntry;
};
type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & {
@@ -36,7 +37,7 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref
User.dismissReferralBanner(referralContentType);
};
- if (!referralContentType || dismissedReferralBanners[referralContentType]) {
+ if (!referralContentType || dismissedReferralBanners?.[referralContentType]) {
return null;
}
@@ -82,7 +83,6 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref
export default withOnyx({
dismissedReferralBanners: {
- key: ONYXKEYS.ACCOUNT,
- selector: (data) => data?.dismissedReferralBanners ?? {},
+ key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
},
})(ReferralProgramCTA);
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index 60dbfc07966a..e9b0ce3dae3f 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -29,14 +29,11 @@ type MoneyReportViewProps = {
/** Policy that the report belongs to */
policy: OnyxEntry;
- /** Policy report fields */
- policyReportFields: PolicyReportField[];
-
/** Whether we should display the horizontal rule below the component */
shouldShowHorizontalRule: boolean;
};
-function MoneyReportView({report, policy, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) {
+function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReportViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -60,9 +57,9 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont
];
const sortedPolicyReportFields = useMemo((): PolicyReportField[] => {
- const fields = ReportUtils.getAvailableReportFields(report, policyReportFields);
+ const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.fieldList ?? {}));
return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight);
- }, [policyReportFields, report]);
+ }, [policy, report]);
return (
@@ -75,13 +72,14 @@ function MoneyReportView({report, policy, policyReportFields, shouldShowHorizont
const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField);
const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue;
const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy);
+ const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID);
return (
{
if (isSplitBillAction) {
@@ -108,14 +110,24 @@ function MoneyRequestAction({
shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport);
}
- return isDeletedParentAction || isReversedTransaction ? (
- ${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`} />
- ) : (
+ if (isDeletedParentAction || isReversedTransaction) {
+ let message: TranslationPaths;
+ if (isReversedTransaction) {
+ message = 'parentReportAction.reversedTransaction';
+ } else if (isTrackExpenseAction) {
+ message = 'parentReportAction.deletedExpense';
+ } else {
+ message = 'parentReportAction.deletedRequest';
+ }
+ return ${translate(message)}`} />;
+ }
+ return (
;
+ return lodashIsEmpty(props.iouReport) && !(props.isBillSplit || props.isTrackExpense) ? null : ;
}
MoneyRequestPreview.displayName = 'MoneyRequestPreview';
diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts
index 17dd42b2f794..3b3eda4ec30a 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts
+++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts
@@ -56,6 +56,9 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & {
/** True if this is this IOU is a split instead of a 1:1 request */
isBillSplit: boolean;
+ /** Whether this IOU is a track expense */
+ isTrackExpense: boolean;
+
/** True if the IOU Preview card is hovered */
isHovered?: boolean;
diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx
index e9bbd0f27bdc..219199c25bc3 100644
--- a/src/components/ReportWelcomeText.tsx
+++ b/src/components/ReportWelcomeText.tsx
@@ -3,6 +3,7 @@ import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
@@ -33,6 +34,7 @@ type ReportWelcomeTextProps = ReportWelcomeTextOnyxProps & {
function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
+ const {canUseTrackExpense} = usePermissions();
const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isChatRoom = ReportUtils.isChatRoom(report);
const isSelfDM = ReportUtils.isSelfDM(report);
@@ -42,7 +44,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant);
const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy);
const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin);
- const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs);
+ const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs, canUseTrackExpense);
const additionalText = moneyRequestOptions.map((item) => translate(`reportActionsView.iouTypes.${item}`)).join(', ');
const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy);
const reportName = ReportUtils.getReportName(report);
@@ -158,9 +160,9 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
))}
)}
- {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)) && (
- {translate('reportActionsView.usePlusButton', {additionalText})}
- )}
+ {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) ||
+ moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST) ||
+ moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK_EXPENSE)) && {translate('reportActionsView.usePlusButton', {additionalText})}}
>
);
diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx
index 4c1f208ce11d..596951374099 100644
--- a/src/components/SelectionList/BaseListItem.tsx
+++ b/src/components/SelectionList/BaseListItem.tsx
@@ -85,7 +85,7 @@ function BaseListItem({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={isDisabled || item.isDisabledCheckbox}
onPress={handleCheckboxPress}
- style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled]}
+ style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.mr3]}
>
{item.isSelected && (
diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx
index ac48b0fa08a9..015fd284c0b4 100644
--- a/src/components/SelectionList/BaseSelectionList.tsx
+++ b/src/components/SelectionList/BaseSelectionList.tsx
@@ -9,6 +9,7 @@ import Button from '@components/Button';
import Checkbox from '@components/Checkbox';
import FixedFooter from '@components/FixedFooter';
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
+import {PressableWithFeedback} from '@components/Pressable';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
import SectionList from '@components/SectionList';
import ShowMoreButton from '@components/ShowMoreButton';
@@ -454,11 +455,22 @@ function BaseSelectionList(
});
/** Calls confirm action when pressing CTRL (CMD) + Enter */
- useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm ?? selectFocusedOption, {
- captureOnInputs: true,
- shouldBubble: !flattenedSections.allOptions[focusedIndex],
- isActive: !disableKeyboardShortcuts && isFocused,
- });
+ useKeyboardShortcut(
+ CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER,
+ (e) => {
+ const focusedOption = flattenedSections.allOptions[focusedIndex];
+ if (onConfirm) {
+ onConfirm(e, focusedOption);
+ return;
+ }
+ selectFocusedOption();
+ },
+ {
+ captureOnInputs: true,
+ shouldBubble: !flattenedSections.allOptions[focusedIndex],
+ isActive: !disableKeyboardShortcuts && isFocused,
+ },
+ );
return (
(
) : (
<>
{!headerMessage && canSelectMultiple && shouldShowSelectAll && (
-
-
- {customListHeader ?? (
-
- {translate('workspace.people.selectAll')}
-
- )}
+
+
+
+ {!customListHeader && (
+ e.preventDefault() : undefined}
+ >
+ {translate('workspace.people.selectAll')}
+
+ )}
+
+ {customListHeader}
)}
{!headerMessage && !canSelectMultiple && customListHeader}
diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts
index 9e9ba7e5fc27..fac78ee786a0 100644
--- a/src/components/SelectionList/types.ts
+++ b/src/components/SelectionList/types.ts
@@ -227,7 +227,7 @@ type BaseSelectionListProps = Partial & {
confirmButtonText?: string;
/** Callback to fire when the confirm button is pressed */
- onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined) => void;
+ onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: TItem) => void;
/** Whether to show the vertical scroll indicator */
showScrollIndicator?: boolean;
diff --git a/src/components/SwipeInterceptPanResponder.tsx b/src/components/SwipeInterceptPanResponder.tsx
index fe1545d2f14b..6a3d14b3b24b 100644
--- a/src/components/SwipeInterceptPanResponder.tsx
+++ b/src/components/SwipeInterceptPanResponder.tsx
@@ -1,6 +1,7 @@
import {PanResponder} from 'react-native';
const SwipeInterceptPanResponder = PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderTerminationRequest: () => false,
});
diff --git a/src/languages/en.ts b/src/languages/en.ts
index cd2d92e25b22..aef5df187a69 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -462,9 +462,14 @@ export default {
copyEmailToClipboard: 'Copy email to clipboard',
markAsUnread: 'Mark as unread',
markAsRead: 'Mark as read',
- editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}`,
- deleteAction: ({action}: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}`,
- deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}?`,
+ editAction: ({action}: EditActionParams) =>
+ `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`,
+ deleteAction: ({action}: DeleteActionParams) =>
+ `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`,
+ deleteConfirmation: ({action}: DeleteConfirmationParams) =>
+ `Are you sure you want to delete this ${
+ action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'
+ }?`,
onlyVisible: 'Only visible to',
replyInThread: 'Reply in thread',
joinThread: 'Join thread',
@@ -503,6 +508,8 @@ export default {
send: 'send money',
split: 'split a bill',
request: 'request money',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'track-expense': 'track an expense',
},
},
reportAction: {
@@ -593,6 +600,7 @@ export default {
participants: 'Participants',
requestMoney: 'Request money',
sendMoney: 'Send money',
+ trackExpense: 'Track expense',
pay: 'Pay',
cancelPayment: 'Cancel payment',
cancelPaymentConfirmation: 'Are you sure that you want to cancel this payment?',
@@ -624,6 +632,7 @@ export default {
finished: 'Finished',
requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`,
requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `requested ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
+ trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`,
didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`,
amountEach: ({amount}: AmountEachParams) => `${amount} each`,
@@ -655,6 +664,7 @@ export default {
updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) =>
`changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`,
threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'request'}`,
+ threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`,
threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`,
tagSelection: 'Select a tag to better organize your spend.',
categorySelection: 'Select a category to better organize your spend.',
@@ -1071,6 +1081,14 @@ export default {
},
},
},
+ workflowsDelayedSubmissionPage: {
+ autoReportingErrorMessage: 'The delayed submission parameter could not be changed. Please try again or contact support.',
+ autoReportingFrequencyErrorMessage: 'The submission frequency could not be changed. Please try again or contact support.',
+ monthlyOffsetErrorMessage: 'The monthly frequency could not be changed. Please try again or contact support.',
+ },
+ workflowsApprovalPage: {
+ genericErrorMessage: 'The approver could not be changed. Please try again or contact support.',
+ },
workflowsPayerPage: {
title: 'Authorized payer',
genericErrorMessage: 'The authorized payer could not be changed. Please try again.',
@@ -1876,7 +1894,19 @@ export default {
errors: {
taxRateAlreadyExists: 'This tax name is already in use.',
valuePercentageRange: 'Please enter a valid percentage between 0 and 100.',
- genericFailureMessage: 'An error occurred while updating the tax rate, please try again.',
+ deleteFailureMessage: 'An error occurred while deleting the tax rate. Please try again or ask Concierge for help.',
+ updateFailureMessage: 'An error occurred while updating the tax rate. Please try again or ask Concierge for help.',
+ createFailureMessage: 'An error occurred while creating the tax rate. Please try again or ask Concierge for help.',
+ },
+ deleteTaxConfirmation: 'Are you sure you want to delete this tax?',
+ deleteMultipleTaxConfirmation: ({taxAmount}) => `Are you sure you want to delete ${taxAmount} taxes?`,
+ actions: {
+ delete: 'Delete rate',
+ deleteMultiple: 'Delete rates',
+ disable: 'Disable rate',
+ disableMultiple: 'Disable rates',
+ enable: 'Enable rate',
+ enableMultiple: 'Enable rates',
},
},
emptyWorkspace: {
@@ -2315,6 +2345,7 @@ export default {
deletedReport: '[Deleted report]',
deletedMessage: '[Deleted message]',
deletedRequest: '[Deleted request]',
+ deletedExpense: '[Deleted expense]',
reversedTransaction: '[Reversed transaction]',
deletedTask: '[Deleted task]',
hiddenMessage: '[Hidden message]',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 3a50e332fd57..5da2ad01bc91 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -453,10 +453,18 @@ export default {
copyEmailToClipboard: 'Copiar email al portapapeles',
markAsUnread: 'Marcar como no leído',
markAsRead: 'Marcar como leído',
- editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`,
- deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`,
+ editAction: ({action}: EditActionParams) =>
+ `Editar ${
+ action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario'
+ }`,
+ deleteAction: ({action}: DeleteActionParams) =>
+ `Eliminar ${
+ action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario'
+ }`,
deleteConfirmation: ({action}: DeleteConfirmationParams) =>
- `¿Estás seguro de que quieres eliminar esta ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`,
+ `¿Estás seguro de que quieres eliminar esta ${
+ action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario'
+ }`,
onlyVisible: 'Visible sólo para',
replyInThread: 'Responder en el hilo',
joinThread: 'Unirse al hilo',
@@ -496,6 +504,8 @@ export default {
send: 'enviar dinero',
split: 'dividir una factura',
request: 'pedir dinero',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'track-expense': 'rastrear un gasto',
},
},
reportAction: {
@@ -586,6 +596,7 @@ export default {
participants: 'Participantes',
requestMoney: 'Pedir dinero',
sendMoney: 'Enviar dinero',
+ trackExpense: 'Seguimiento de gastos',
pay: 'Pagar',
cancelPayment: 'Cancelar el pago',
cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?',
@@ -617,6 +628,7 @@ export default {
finished: 'Finalizado',
requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`,
requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicité ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
+ trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `seguimiento ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`,
didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`,
amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`,
@@ -650,6 +662,7 @@ export default {
updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) =>
`cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`,
threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Solicitud de ${formattedAmount}`}`,
+ threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`,
threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`,
tagSelection: 'Selecciona una etiqueta para organizar mejor tu dinero.',
categorySelection: 'Seleccione una categoría para organizar mejor tu dinero.',
@@ -1067,6 +1080,14 @@ export default {
},
},
},
+ workflowsDelayedSubmissionPage: {
+ autoReportingErrorMessage: 'El parámetro de envío retrasado no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.',
+ autoReportingFrequencyErrorMessage: 'La frecuencia de envío no pudo ser cambiada. Por favor, inténtelo de nuevo o contacte al soporte.',
+ monthlyOffsetErrorMessage: 'La frecuencia mensual no pudo ser cambiada. Por favor, inténtelo de nuevo o contacte al soporte.',
+ },
+ workflowsApprovalPage: {
+ genericErrorMessage: 'El aprobador no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.',
+ },
workflowsPayerPage: {
title: 'Pagador autorizado',
genericErrorMessage: 'El pagador autorizado no se pudo cambiar. Por favor, inténtalo mas tarde.',
@@ -1899,8 +1920,20 @@ export default {
value: 'Valor',
errors: {
taxRateAlreadyExists: 'Ya existe un impuesto con este nombre',
- valuePercentageRange: 'Introduzca un porcentaje válido entre 0 y 100',
- genericFailureMessage: 'Se produjo un error al actualizar el tipo impositivo, inténtelo nuevamente.',
+ valuePercentageRange: 'Por favor, introduce un porcentaje entre 0 y 100',
+ deleteFailureMessage: 'Se ha producido un error al intentar eliminar la tasa de impuesto. Por favor, inténtalo más tarde.',
+ updateFailureMessage: 'Se ha producido un error al intentar modificar la tasa de impuesto. Por favor, inténtalo más tarde.',
+ createFailureMessage: 'Se ha producido un error al intentar crear la tasa de impuesto. Por favor, inténtalo más tarde.',
+ },
+ deleteTaxConfirmation: '¿Estás seguro de que quieres eliminar este impuesto?',
+ deleteMultipleTaxConfirmation: ({taxAmount}) => `¿Estás seguro de que quieres eliminar ${taxAmount} impuestos?`,
+ actions: {
+ delete: 'Eliminar tasa',
+ deleteMultiple: 'Eliminar tasas',
+ disable: 'Desactivar tasa',
+ disableMultiple: 'Desactivar tasas',
+ enable: 'Activar tasa',
+ enableMultiple: 'Activar tasas',
},
},
emptyWorkspace: {
@@ -2803,6 +2836,7 @@ export default {
deletedReport: '[Informe eliminado]',
deletedMessage: '[Mensaje eliminado]',
deletedRequest: '[Solicitud eliminada]',
+ deletedExpense: '[Gasto eliminado]',
reversedTransaction: '[Transacción anulada]',
deletedTask: '[Tarea eliminada]',
hiddenMessage: '[Mensaje oculto]',
diff --git a/src/libs/API/parameters/DeletePolicyTaxesParams.ts b/src/libs/API/parameters/DeletePolicyTaxesParams.ts
new file mode 100644
index 000000000000..9e0963cdcb28
--- /dev/null
+++ b/src/libs/API/parameters/DeletePolicyTaxesParams.ts
@@ -0,0 +1,11 @@
+type DeletePolicyTaxesParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array
+ * Each element is a tax name
+ */
+ taxNames: string;
+};
+
+export default DeletePolicyTaxesParams;
diff --git a/src/libs/API/parameters/RenamePolicyTaxParams.ts b/src/libs/API/parameters/RenamePolicyTaxParams.ts
new file mode 100644
index 000000000000..b722f14e7b6e
--- /dev/null
+++ b/src/libs/API/parameters/RenamePolicyTaxParams.ts
@@ -0,0 +1,7 @@
+type SetPolicyCurrencyDefaultParams = {
+ policyID: string;
+ taxCode: string;
+ newName: string;
+};
+
+export default SetPolicyCurrencyDefaultParams;
diff --git a/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts
new file mode 100644
index 000000000000..4ed0a05cfdec
--- /dev/null
+++ b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts
@@ -0,0 +1,10 @@
+type SetPolicyTaxesEnabledParams = {
+ policyID: string;
+ /**
+ * Stringified JSON object with type of following structure:
+ * Array<{taxCode: string, enabled: bool}>
+ */
+ taxFieldsArray: string;
+};
+
+export default SetPolicyTaxesEnabledParams;
diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts
new file mode 100644
index 000000000000..f48c8666f109
--- /dev/null
+++ b/src/libs/API/parameters/TrackExpenseParams.ts
@@ -0,0 +1,30 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+import type {Receipt} from '@src/types/onyx/Transaction';
+
+type TrackExpenseParams = {
+ amount: number;
+ currency: string;
+ comment: string;
+ created: string;
+ merchant: string;
+ iouReportID?: string;
+ chatReportID: string;
+ transactionID: string;
+ reportActionID: string;
+ createdChatReportActionID: string;
+ createdIOUReportActionID?: string;
+ reportPreviewReportActionID?: string;
+ receipt: Receipt;
+ receiptState?: ValueOf;
+ category?: string;
+ tag?: string;
+ taxCode: string;
+ taxAmount: number;
+ billable?: boolean;
+ gpsPoints?: string;
+ transactionThreadReportID: string;
+ createdReportActionIDForThread: string;
+};
+
+export default TrackExpenseParams;
diff --git a/src/libs/API/parameters/UpdatePolicyTaxValueParams.ts b/src/libs/API/parameters/UpdatePolicyTaxValueParams.ts
new file mode 100644
index 000000000000..1124755ea9ef
--- /dev/null
+++ b/src/libs/API/parameters/UpdatePolicyTaxValueParams.ts
@@ -0,0 +1,7 @@
+type UpdatePolicyTaxValueParams = {
+ policyID: string;
+ taxCode: string;
+ taxAmount: number;
+};
+
+export default UpdatePolicyTaxValueParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 84ace32d6261..0049489f1fc2 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -164,6 +164,7 @@ export type {default as SetWorkspaceReimbursementParams} from './SetWorkspaceRei
export type {default as SetPolicyRequiresTag} from './SetPolicyRequiresTag';
export type {default as RenamePolicyTaglist} from './RenamePolicyTaglist';
export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams';
+export type {default as TrackExpenseParams} from './TrackExpenseParams';
export type {default as EnablePolicyCategoriesParams} from './EnablePolicyCategoriesParams';
export type {default as EnablePolicyConnectionsParams} from './EnablePolicyConnectionsParams';
export type {default as EnablePolicyDistanceRatesParams} from './EnablePolicyDistanceRatesParams';
@@ -184,8 +185,12 @@ export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDist
export type {default as SetPolicyDistanceRatesUnitParams} from './SetPolicyDistanceRatesUnitParams';
export type {default as SetPolicyDistanceRatesDefaultCategoryParams} from './SetPolicyDistanceRatesDefaultCategoryParams';
export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams';
+export type {default as SetPolicyTaxesEnabledParams} from './SetPolicyTaxesEnabledParams';
+export type {default as DeletePolicyTaxesParams} from './DeletePolicyTaxesParams';
+export type {default as UpdatePolicyTaxValueParams} from './UpdatePolicyTaxValueParams';
export type {default as RenamePolicyTagsParams} from './RenamePolicyTagsParams';
export type {default as DeletePolicyTagsParams} from './DeletePolicyTagsParams';
export type {default as SetPolicyCustomTaxNameParams} from './SetPolicyCustomTaxNameParams';
export type {default as SetPolicyForeignCurrencyDefaultParams} from './SetPolicyForeignCurrencyDefaultParams';
export type {default as SetPolicyCurrencyDefaultParams} from './SetPolicyCurrencyDefaultParams';
+export type {default as RenamePolicyTaxParams} from './RenamePolicyTaxParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 8d359febfd0f..cc524f987fab 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -168,6 +168,7 @@ const WRITE_COMMANDS = {
CANCEL_PAYMENT: 'CancelPayment',
ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount',
SWITCH_TO_OLD_DOT: 'SwitchToOldDot',
+ TRACK_EXPENSE: 'TrackExpense',
ENABLE_POLICY_CATEGORIES: 'EnablePolicyCategories',
ENABLE_POLICY_CONNECTIONS: 'EnablePolicyConnections',
ENABLE_POLICY_DISTANCE_RATES: 'EnablePolicyDistanceRates',
@@ -182,6 +183,10 @@ const WRITE_COMMANDS = {
ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest',
DECLINE_JOIN_REQUEST: 'DeclineJoinRequest',
CREATE_POLICY_TAX: 'CreatePolicyTax',
+ SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled',
+ DELETE_POLICY_TAXES: 'DeletePolicyTaxes',
+ UPDATE_POLICY_TAX_VALUE: 'UpdatePolicyTaxValue',
+ RENAME_POLICY_TAX: 'RenamePolicyTax',
CREATE_POLICY_DISTANCE_RATE: 'CreatePolicyDistanceRate',
SET_POLICY_DISTANCE_RATES_UNIT: 'SetPolicyDistanceRatesUnit',
SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY: 'SetPolicyDistanceRatesDefaultCategory',
@@ -351,6 +356,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_WORKSPACE_PAYER]: Parameters.SetWorkspacePayerParams;
[WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT]: Parameters.SetWorkspaceReimbursementParams;
[WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams;
+ [WRITE_COMMANDS.TRACK_EXPENSE]: Parameters.TrackExpenseParams;
[WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES]: Parameters.EnablePolicyCategoriesParams;
[WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS]: Parameters.EnablePolicyConnectionsParams;
[WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES]: Parameters.EnablePolicyDistanceRatesParams;
@@ -365,7 +371,11 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.SET_POLICY_CUSTOM_TAX_NAME]: Parameters.SetPolicyCustomTaxNameParams;
[WRITE_COMMANDS.SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT]: Parameters.SetPolicyForeignCurrencyDefaultParams;
[WRITE_COMMANDS.CREATE_POLICY_TAX]: Parameters.CreatePolicyTaxParams;
+ [WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED]: Parameters.SetPolicyTaxesEnabledParams;
+ [WRITE_COMMANDS.DELETE_POLICY_TAXES]: Parameters.DeletePolicyTaxesParams;
+ [WRITE_COMMANDS.UPDATE_POLICY_TAX_VALUE]: Parameters.UpdatePolicyTaxValueParams;
[WRITE_COMMANDS.CREATE_POLICY_DISTANCE_RATE]: Parameters.CreatePolicyDistanceRateParams;
+ [WRITE_COMMANDS.RENAME_POLICY_TAX]: Parameters.RenamePolicyTaxParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams;
[WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams;
};
diff --git a/src/libs/ErrorUtils.ts b/src/libs/ErrorUtils.ts
index 784d339a4a0d..d38700efd53d 100644
--- a/src/libs/ErrorUtils.ts
+++ b/src/libs/ErrorUtils.ts
@@ -110,6 +110,21 @@ function getEarliestErrorField(onyxDa
return {[key]: getErrorMessageWithTranslationData(errorsForField[key])};
}
+/**
+ * Method used to get the latest error field for any field
+ */
+function getLatestErrorFieldForAnyField(onyxData: TOnyxData): Errors {
+ const errorFields = onyxData.errorFields ?? {};
+
+ if (Object.keys(errorFields).length === 0) {
+ return {};
+ }
+
+ const fieldNames = Object.keys(errorFields);
+ const latestErrorFields = fieldNames.map((fieldName) => getLatestErrorField(onyxData, fieldName));
+ return latestErrorFields.reduce((acc, error) => ({...acc, ...error}), {});
+}
+
/**
* Method used to attach already translated message with isTranslated property
* @param errors - An object containing current errors in the form
@@ -176,6 +191,7 @@ export {
getLatestErrorField,
getLatestErrorMessage,
getLatestErrorMessageField,
+ getLatestErrorFieldForAnyField,
getMicroSecondOnyxError,
getMicroSecondOnyxErrorObject,
isReceiptError,
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index 56ac47676a37..07bb22f43b31 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -104,7 +104,7 @@ function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean {
* Checks if the iou type is one of request, send, or split.
*/
function isValidMoneyRequestType(iouType: string): boolean {
- const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND];
+ const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.TRACK_EXPENSE];
return moneyRequestType.includes(iouType);
}
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
index b576f0e7601a..bd5bfc46134a 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
@@ -284,6 +284,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/settings/ExitSurvey/ExitSurveyConfirmPage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../pages/workspace/taxes/WorkspaceEditTaxPage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TAX_NAME]: () => require('../../../pages/workspace/taxes/NamePage').default as React.ComponentType,
+ [SCREENS.WORKSPACE.TAX_VALUE]: () => require('../../../pages/workspace/taxes/ValuePage').default as React.ComponentType,
[SCREENS.WORKSPACE.TAX_CREATE]: () => require('../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default as React.ComponentType,
});
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index 7388d6447ffa..17f5049aab91 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -17,6 +17,10 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.TAXES_SETTINGS_CUSTOM_TAX_NAME,
SCREENS.WORKSPACE.TAXES_SETTINGS_FOREIGN_CURRENCY_DEFAULT,
SCREENS.WORKSPACE.TAXES_SETTINGS_WORKSPACE_CURRENCY_DEFAULT,
+ SCREENS.WORKSPACE.TAX_CREATE,
+ SCREENS.WORKSPACE.TAX_EDIT,
+ SCREENS.WORKSPACE.TAX_NAME,
+ SCREENS.WORKSPACE.TAX_VALUE,
],
[SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE, SCREENS.WORKSPACE.TAG_SETTINGS, SCREENS.WORKSPACE.TAG_EDIT],
[SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS, SCREENS.WORKSPACE.CATEGORY_EDIT],
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 391d584d5a78..130fdf23732f 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -354,6 +354,15 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.TAX_CREATE]: {
path: ROUTES.WORKSPACE_TAX_CREATE.route,
},
+ [SCREENS.WORKSPACE.TAX_EDIT]: {
+ path: ROUTES.WORKSPACE_TAX_EDIT.route,
+ },
+ [SCREENS.WORKSPACE.TAX_NAME]: {
+ path: ROUTES.WORKSPACE_TAX_NAME.route,
+ },
+ [SCREENS.WORKSPACE.TAX_VALUE]: {
+ path: ROUTES.WORKSPACE_TAX_VALUE.route,
+ },
},
},
[SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 90fad4f29f22..9b0d9ce4decc 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -242,6 +242,18 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.TAX_CREATE]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.TAX_EDIT]: {
+ policyID: string;
+ taxID: string;
+ };
+ [SCREENS.WORKSPACE.TAX_NAME]: {
+ policyID: string;
+ taxID: string;
+ };
+ [SCREENS.WORKSPACE.TAX_VALUE]: {
+ policyID: string;
+ taxID: string;
+ };
} & ReimbursementAccountNavigatorParamList;
type NewChatNavigatorParamList = {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index bacd019904a3..b230cbb1dd00 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -249,9 +249,11 @@ Onyx.connect({
});
const policyExpenseReports: OnyxCollection = {};
+const allReports: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
callback: (report, key) => {
+ allReports[key] = report;
if (!ReportUtils.isPolicyExpenseChat(report)) {
return;
}
@@ -734,6 +736,35 @@ function createOption(
return result;
}
+/**
+ * Get the option for a given report.
+ */
+function getReportOption(participant: Participant): ReportUtils.OptionData {
+ const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`];
+
+ const option = createOption(
+ report?.visibleChatMemberAccountIDs ?? [],
+ allPersonalDetails ?? {},
+ report ?? null,
+ {},
+ {
+ showChatPreviewLine: false,
+ forcePolicyNamePreview: false,
+ },
+ );
+
+ // Update text & alternateText because createOption returns workspace name only if report is owned by the user
+ if (option.isSelfDM) {
+ option.alternateText = Localize.translateLocal('reportActionsView.yourSpace');
+ } else {
+ option.text = ReportUtils.getPolicyName(report);
+ option.alternateText = Localize.translateLocal('workspace.common.workspace');
+ }
+ option.selected = participant.selected;
+ option.isSelected = participant.selected;
+ return option;
+}
+
/**
* Get the option for a policy expense report.
*/
@@ -2073,6 +2104,7 @@ export {
formatSectionsFromSearchTerm,
transformedTaxRates,
getShareLogOptions,
+ getReportOption,
getTaxRatesSection,
};
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index 26df03134fd5..071113a70fae 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -26,6 +26,10 @@ function canUseViolations(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas);
}
+function canUseTrackExpense(betas: OnyxEntry): boolean {
+ return !!betas?.includes(CONST.BETAS.TRACK_EXPENSE) || canUseAllBetas(betas);
+}
+
function canUseP2PDistanceRequests(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas);
}
@@ -47,6 +51,7 @@ export default {
canUseCommentLinking,
canUseLinkPreviews,
canUseViolations,
+ canUseTrackExpense,
canUseReportFields,
canUseP2PDistanceRequests,
canUseWorkflowsDelayedSubmission,
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 675e268045c1..39e6c8932aad 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -4,7 +4,8 @@ import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
-import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx';
+import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx';
+import type {PolicyFeatureName} from '@src/types/onyx/Policy';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Navigation from './Navigation/Navigation';
@@ -34,7 +35,7 @@ function hasPolicyMemberError(policyMembers: OnyxEntry): boolean
* Check if the policy has any tax rate errors.
*/
function hasTaxRateError(policy: OnyxEntry): boolean {
- return Object.values(policy?.taxRates?.taxes ?? {}).some((taxRate) => Object.keys(taxRate?.errors ?? {}).length > 0);
+ return Object.values(policy?.taxRates?.taxes ?? {}).some((taxRate) => Object.keys(taxRate?.errors ?? {}).length > 0 || Object.values(taxRate?.errorFields ?? {}).some(Boolean));
}
/**
@@ -276,6 +277,26 @@ function goBackFromInvalidPolicy() {
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
}
+/** Get a tax with given ID from policy */
+function getTaxByID(policy: OnyxEntry, taxID: string): TaxRate | undefined {
+ return policy?.taxRates?.taxes?.[taxID];
+}
+
+/**
+ * Whether the tax rate can be deleted and disabled
+ */
+function canEditTaxRate(policy: Policy, taxID: string): boolean {
+ return policy.taxRates?.defaultExternalID !== taxID;
+}
+
+function isPolicyFeatureEnabled(policy: OnyxEntry | EmptyObject, featureName: PolicyFeatureName): boolean {
+ if (featureName === CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED) {
+ return Boolean(policy?.tax?.trackingEnabled);
+ }
+
+ return Boolean(policy?.[featureName]);
+}
+
export {
getActivePolicies,
hasAccountingConnections,
@@ -296,6 +317,7 @@ export {
getIneligibleInvitees,
getTagLists,
getTagListName,
+ canEditTaxRate,
getTagList,
getCleanedTagName,
getCountOfEnabledTagsOfList,
@@ -306,7 +328,9 @@ export {
getPathWithoutPolicyID,
getPolicyMembersByIdWithoutCurrentUser,
goBackFromInvalidPolicy,
+ isPolicyFeatureEnabled,
hasTaxRateError,
+ getTaxByID,
hasPolicyCategoriesError,
};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index fbee7ffd7b11..206c3ecd75a6 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -209,6 +209,7 @@ function isTransactionThread(parentReportAction: OnyxEntry | Empty
return (
parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
(parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE ||
+ parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ||
(parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !!parentReportAction.originalMessage.IOUDetails))
);
}
@@ -649,7 +650,7 @@ function getReportPreviewAction(chatReportID: string, iouReportID: string): Onyx
* Get the iouReportID for a given report action.
*/
function getIOUReportIDFromReportActionPreview(reportAction: OnyxEntry): string {
- return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? reportAction.originalMessage.linkedReportID : '';
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? reportAction.originalMessage.linkedReportID : '0';
}
function isCreatedTaskReportAction(reportAction: OnyxEntry): boolean {
@@ -674,6 +675,10 @@ function isSplitBillAction(reportAction: OnyxEntry): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;
}
+function isTrackExpenseAction(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && (reportAction.originalMessage as IOUMessage).type === CONST.IOU.REPORT_ACTION_TYPE.TRACK;
+}
+
function isTaskAction(reportAction: OnyxEntry): boolean {
const reportActionName = reportAction?.actionName;
return (
@@ -996,6 +1001,7 @@ export {
isReportPreviewAction,
isSentMoneyReportAction,
isSplitBillAction,
+ isTrackExpenseAction,
isTaskAction,
doesReportHaveVisibleActions,
isThreadParentMessage,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index b5351e13ea6c..14041fbe35ce 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -23,7 +23,6 @@ import type {
PersonalDetailsList,
Policy,
PolicyReportField,
- PolicyReportFields,
Report,
ReportAction,
ReportMetadata,
@@ -482,14 +481,6 @@ Onyx.connect({
callback: (value) => (allPolicies = value),
});
-let allPolicyReportFields: OnyxCollection = {};
-
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS,
- waitForCollectionCallback: true,
- callback: (value) => (allPolicyReportFields = value),
-});
-
let allBetas: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.BETAS,
@@ -938,6 +929,15 @@ function isConciergeChatReport(report: OnyxEntry): boolean {
return report?.participantAccountIDs?.length === 1 && Number(report.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report);
}
+function findSelfDMReportID(): string | undefined {
+ if (!allReports) {
+ return;
+ }
+
+ const selfDMReport = Object.values(allReports).find((report) => isSelfDM(report) && !isThread(report));
+ return selfDMReport?.reportID;
+}
+
/**
* Checks if the supplied report belongs to workspace based on the provided params. If the report's policyID is _FAKE_ or has no value, it means this report is a DM.
* In this case report and workspace members must be compared to determine whether the report belongs to the workspace.
@@ -1227,6 +1227,18 @@ function isIOURequest(report: OnyxEntry): boolean {
return false;
}
+/**
+ * A Track Expense Report is a thread where the parent the parentReportAction is a transaction, and
+ * parentReportAction has type of track.
+ */
+function isTrackExpenseReport(report: OnyxEntry): boolean {
+ if (isThread(report)) {
+ const parentReportAction = ReportActionsUtils.getParentReportAction(report);
+ return !isEmptyObject(parentReportAction) && ReportActionsUtils.isTrackExpenseAction(parentReportAction);
+ }
+ return false;
+}
+
/**
* Checks if a report is an IOU or expense request.
*/
@@ -2078,22 +2090,36 @@ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry
/**
* Given a set of report fields, return the field of type formula
*/
-function getFormulaTypeReportField(reportFields: PolicyReportFields) {
- return Object.values(reportFields).find((field) => field.type === 'formula');
+function getFormulaTypeReportField(reportFields: Record) {
+ return Object.values(reportFields).find((field) => field?.type === 'formula');
}
/**
* Given a set of report fields, return the field that refers to title
*/
-function getTitleReportField(reportFields: PolicyReportFields) {
+function getTitleReportField(reportFields: Record) {
return Object.values(reportFields).find((field) => isReportFieldOfTypeTitle(field));
}
+/**
+ * Get the key for a report field
+ */
+function getReportFieldKey(reportFieldId: string) {
+ return `expensify_${reportFieldId}`;
+}
+
/**
* Get the report fields attached to the policy given policyID
*/
-function getReportFieldsByPolicyID(policyID: string) {
- return Object.entries(allPolicyReportFields ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS, '') === policyID)?.[1];
+function getReportFieldsByPolicyID(policyID: string): Record {
+ const policyReportFields = Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID);
+ const fieldList = policyReportFields?.[1]?.fieldList;
+
+ if (!policyReportFields || !fieldList) {
+ return {};
+ }
+
+ return fieldList;
}
/**
@@ -2102,7 +2128,7 @@ function getReportFieldsByPolicyID(policyID: string) {
function getAvailableReportFields(report: Report, policyReportFields: PolicyReportField[]): PolicyReportField[] {
// Get the report fields that are attached to a report. These will persist even if a field is deleted from the policy.
- const reportFields = Object.values(report.reportFields ?? {});
+ const reportFields = Object.values(report.fieldList ?? {});
const reportIsSettled = isSettled(report.reportID);
// If the report is settled, we don't want to show any new field that gets added to the policy.
@@ -2113,7 +2139,24 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo
// If the report is unsettled, we want to merge the new fields that get added to the policy with the fields that
// are attached to the report.
const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)]));
- return mergedFieldIds.map((id) => report?.reportFields?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[];
+
+ const fields = mergedFieldIds.map((id) => {
+ const field = report?.fieldList?.[getReportFieldKey(id)];
+
+ if (field) {
+ return field;
+ }
+
+ const policyReportField = policyReportFields.find(({fieldID}) => fieldID === id);
+
+ if (policyReportField) {
+ return policyReportField;
+ }
+
+ return null;
+ });
+
+ return fields.filter(Boolean) as PolicyReportField[];
}
/**
@@ -2121,7 +2164,7 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo
*/
function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string {
const isReportSettled = isSettled(report?.reportID ?? '');
- const reportFields = isReportSettled ? report?.reportFields : getReportFieldsByPolicyID(report?.policyID ?? '');
+ const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID ?? '');
const titleReportField = getFormulaTypeReportField(reportFields ?? {});
if (titleReportField && report?.reportName && reportFieldsEnabled(report)) {
@@ -2211,6 +2254,11 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean {
return true;
}
+ // TODO: Uncomment this line when BE starts working properly (Editing Track Expense)
+ // if (reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK) {
+ // return true;
+ // }
+
if (reportAction.originalMessage.type !== CONST.IOU.REPORT_ACTION_TYPE.CREATE) {
return false;
}
@@ -2382,10 +2430,25 @@ function getTransactionReportName(reportAction: OnyxEntry, policy: OnyxEntry, o
/**
* Helper method to define what money request options we want to show for particular method.
- * There are 3 money request options: Request, Split and Send:
+ * There are 4 money request options: Request, Split, Send and Track expense:
* - Request option should show for:
* - DMs
* - own policy expense chats
@@ -4561,13 +4649,16 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o
* - chat/ policy rooms with more than 1 participants
* - groups chats with 3 and more participants
* - corporate workspace chats
+ * - Track expense option should show for:
+ * - Self DMs
+ * - admin rooms
*
* None of the options should show in chat threads or if there is some special Expensify account
* as a participant of the report.
*/
-function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[]): Array> {
+function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[], canUseTrackExpense = true): Array> {
// In any thread or task report, we do not allow any new money requests yet
- if (isChatThread(report) || isTaskReport(report) || isSelfDM(report)) {
+ if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report))) {
return [];
}
@@ -4583,6 +4674,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry 1;
let options: Array> = [];
+ if (isSelfDM(report)) {
+ options = [CONST.IOU.TYPE.TRACK_EXPENSE];
+ }
+
// User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option
// unless there are no other participants at all (e.g. #admins room for a policy with only 1 admin)
// DM chats will have the Split Bill option only when there are at least 2 other people in the chat.
@@ -4591,6 +4686,11 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry)
amount: formattedAmount,
});
}
- translationKey = ReportActionsUtils.isSplitBillAction(reportAction) ? 'iou.didSplitAmount' : 'iou.requestedAmount';
+ if (ReportActionsUtils.isSplitBillAction(reportAction)) {
+ translationKey = 'iou.didSplitAmount';
+ } else if (ReportActionsUtils.isTrackExpenseAction(reportAction)) {
+ translationKey = 'iou.trackedAmount';
+ } else {
+ translationKey = 'iou.requestedAmount';
+ }
return Localize.translateLocal(translationKey, {
formattedAmount,
comment: transactionDetails?.comment ?? '',
@@ -5539,16 +5645,19 @@ export {
hasUpdatedTotal,
isReportFieldDisabled,
getAvailableReportFields,
+ getReportFieldKey,
reportFieldsEnabled,
getAllAncestorReportActionIDs,
getPendingChatMembers,
canEditRoomVisibility,
canEditPolicyDescription,
getPolicyDescriptionText,
+ findSelfDMReportID,
getIndicatedMissingPaymentMethod,
isJoinRequestInAdminRoom,
canAddOrDeleteTransactions,
shouldCreateNewMoneyRequestReport,
+ isTrackExpenseReport,
hasActionsWithErrors,
};
diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts
index 5876ccf5d7d7..cacab8333868 100644
--- a/src/libs/ValidationUtils.ts
+++ b/src/libs/ValidationUtils.ts
@@ -471,8 +471,9 @@ function isValidPercentage(value: string): boolean {
/**
* Validates the given value if it is correct tax name.
*/
-function isExistingTaxName(value: string, taxRates: TaxRates): boolean {
- return !!Object.values(taxRates).find((taxRate) => taxRate.name === value);
+function isExistingTaxName(taxName: string, taxRates: TaxRates): boolean {
+ const trimmedTaxName = taxName.trim();
+ return !!Object.values(taxRates).find((taxRate) => taxRate.name === trimmedTaxName);
}
export {
diff --git a/src/libs/WorkspacesSettingsUtils.ts b/src/libs/WorkspacesSettingsUtils.ts
index 23cb53a317b0..f808f602a1c6 100644
--- a/src/libs/WorkspacesSettingsUtils.ts
+++ b/src/libs/WorkspacesSettingsUtils.ts
@@ -92,8 +92,9 @@ function hasGlobalWorkspaceSettingsRBR(policies: OnyxCollection, policyM
function hasWorkspaceSettingsRBR(policy: Policy) {
const policyMemberError = allPolicyMembers ? hasPolicyMemberError(allPolicyMembers[`${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policy.id}`]) : false;
+ const taxRateError = hasTaxRateError(policy);
- return Object.keys(reimbursementAccount?.errors ?? {}).length > 0 || hasPolicyError(policy) || hasCustomUnitsError(policy) || policyMemberError;
+ return Object.keys(reimbursementAccount?.errors ?? {}).length > 0 || hasPolicyError(policy) || hasCustomUnitsError(policy) || policyMemberError || taxRateError;
}
function getChatTabBrickRoad(policyID?: string): BrickRoad | undefined {
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index 562ed501eebc..94b794241ffa 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -22,6 +22,7 @@ import type {
SplitBillParams,
StartSplitBillParams,
SubmitReportParams,
+ TrackExpenseParams,
UpdateMoneyRequestParams,
} from '@libs/API/parameters';
import {WRITE_COMMANDS} from '@libs/API/types';
@@ -84,6 +85,19 @@ type MoneyRequestInformation = {
onyxData: OnyxData;
};
+type TrackExpenseInformation = {
+ iouReport?: OnyxTypes.Report;
+ chatReport: OnyxTypes.Report;
+ transaction: OnyxTypes.Transaction;
+ iouAction: OptimisticIOUReportAction;
+ createdChatReportActionID: string;
+ createdIOUReportActionID?: string;
+ reportPreviewAction?: OnyxTypes.ReportAction;
+ transactionThreadReportID: string;
+ createdReportActionIDForThread: string;
+ onyxData: OnyxData;
+};
+
type SplitData = {
chatReportID: string;
transactionID: string;
@@ -794,6 +808,178 @@ function buildOnyxDataForMoneyRequest(
return [optimisticData, successData, failureData];
}
+/** Builds the Onyx data for track expense */
+function buildOnyxDataForTrackExpense(
+ chatReport: OnyxEntry,
+ transaction: OnyxTypes.Transaction,
+ iouAction: OptimisticIOUReportAction,
+ transactionThreadReport: OptimisticChatReport,
+ transactionThreadCreatedReportAction: OptimisticCreatedReportAction,
+ policy?: OnyxEntry,
+ policyTagList?: OnyxEntry,
+ policyCategories?: OnyxEntry,
+): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] {
+ const isScanRequest = TransactionUtils.isScanRequest(transaction);
+ const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null]));
+ const optimisticData: OnyxUpdate[] = [];
+
+ if (chatReport) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`,
+ value: {
+ ...chatReport,
+ lastMessageText: iouAction.message?.[0].text,
+ lastMessageHtml: iouAction.message?.[0].html,
+ lastReadTime: DateUtils.getDBTime(),
+ },
+ });
+ }
+
+ optimisticData.push(
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ value: transaction,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ value: {
+ [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
+ value: transactionThreadReport,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ value: {
+ [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction,
+ },
+ },
+
+ // Remove the temporary transaction used during the creation flow
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`,
+ value: null,
+ },
+ );
+
+ const successData: OnyxUpdate[] = [];
+
+ successData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
+ value: {
+ pendingFields: null,
+ errorFields: null,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ value: {
+ pendingAction: null,
+ pendingFields: clearedPendingFields,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ value: {
+ [iouAction.reportActionID]: {
+ pendingAction: null,
+ errors: null,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ value: {
+ [transactionThreadCreatedReportAction.reportActionID]: {
+ pendingAction: null,
+ errors: null,
+ },
+ },
+ },
+ );
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`,
+ value: {
+ lastReadTime: chatReport?.lastReadTime,
+ lastMessageText: chatReport?.lastMessageText,
+ lastMessageHtml: chatReport?.lastMessageHtml,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`,
+ value: {
+ errorFields: {
+ createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
+ value: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ pendingAction: null,
+ pendingFields: clearedPendingFields,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ value: {
+ [iouAction.reportActionID]: {
+ // Disabling this line since transaction.filename can be an empty string
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`,
+ value: {
+ [transactionThreadCreatedReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ },
+ },
+ },
+ ];
+
+ // We don't need to compute violations unless we're on a paid policy
+ if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) {
+ return [optimisticData, successData, failureData];
+ }
+
+ const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {});
+
+ if (violationsOnyxData) {
+ optimisticData.push(violationsOnyxData);
+ failureData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
+ value: [],
+ });
+ }
+
+ return [optimisticData, successData, failureData];
+}
+
/**
* Gathers all the data needed to make a money request. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then
* it creates optimistic versions of them and uses those instead
@@ -1002,6 +1188,137 @@ function getMoneyRequestInformation(
};
}
+/**
+ * Gathers all the data needed to make an expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then
+ * it creates optimistic versions of them and uses those instead
+ */
+function getTrackExpenseInformation(
+ parentChatReport: OnyxEntry | EmptyObject,
+ participant: Participant,
+ comment: string,
+ amount: number,
+ currency: string,
+ created: string,
+ merchant: string,
+ receipt: Receipt | undefined,
+ category: string | undefined,
+ tag: string | undefined,
+ billable: boolean | undefined,
+ policy: OnyxEntry | undefined,
+ policyTagList: OnyxEntry | undefined,
+ policyCategories: OnyxEntry | undefined,
+ payeeEmail = currentUserEmail,
+): TrackExpenseInformation | EmptyObject {
+ // STEP 1: Get existing chat report
+ let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null;
+
+ // The chatReport always exist and we can get it from Onyx if chatReport is null.
+ if (!chatReport) {
+ chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`] ?? null;
+ }
+
+ // If we still don't have a report, it likely doens't exist and we will early return here as it should not happen
+ // Maybe later, we can build an optimistic selfDM chat.
+ if (!chatReport) {
+ return {};
+ }
+
+ // STEP 2: Get the money request report.
+ // TODO: This is deferred to later as we are not sure if we create iouReport at all in future.
+ // We can build an optimistic iouReport here if needed.
+
+ // STEP 3: Build optimistic receipt and transaction
+ const receiptObject: Receipt = {};
+ let filename;
+ if (receipt?.source) {
+ receiptObject.source = receipt.source;
+ receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY;
+ filename = receipt.name;
+ }
+ const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`];
+ const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE;
+ let optimisticTransaction = TransactionUtils.buildOptimisticTransaction(
+ amount,
+ currency,
+ chatReport.reportID,
+ comment,
+ created,
+ '',
+ '',
+ merchant,
+ receiptObject,
+ filename,
+ null,
+ category,
+ tag,
+ billable,
+ isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined,
+ );
+
+ // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction
+ // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction
+ // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109.
+ // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417
+ // to remind me to do this.
+ if (isDistanceRequest) {
+ optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction, false);
+ }
+
+ // STEP 4: Build optimistic reportActions. We need:
+ // 1. IOU action for the chatReport
+ // 2. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread
+ const currentTime = DateUtils.getDBTime();
+ const iouAction = ReportUtils.buildOptimisticIOUReportAction(
+ CONST.IOU.REPORT_ACTION_TYPE.TRACK,
+ amount,
+ currency,
+ comment,
+ [participant],
+ optimisticTransaction.transactionID,
+ undefined,
+ '0',
+ false,
+ false,
+ receiptObject,
+ false,
+ currentTime,
+ );
+ const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport);
+ const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail);
+
+ // The IOU action and the transactionThread are co-dependent as parent-child, so we need to link them together
+ iouAction.childReportID = optimisticTransactionThread.reportID;
+
+ // STEP 5: Build Onyx Data
+ const [optimisticData, successData, failureData] = buildOnyxDataForTrackExpense(
+ chatReport,
+ optimisticTransaction,
+ iouAction,
+ optimisticTransactionThread,
+ optimisticCreatedActionForTransactionThread,
+ policy,
+ policyTagList,
+ policyCategories,
+ );
+
+ return {
+ chatReport,
+ iouReport: undefined,
+ transaction: optimisticTransaction,
+ iouAction,
+ createdChatReportActionID: '0',
+ createdIOUReportActionID: undefined,
+ reportPreviewAction: undefined,
+ transactionThreadReportID: optimisticTransactionThread.reportID,
+ createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID,
+ onyxData: {
+ optimisticData,
+ successData,
+ failureData,
+ },
+ };
+}
+
/** Requests money based on a distance (eg. mileage from a map) */
function createDistanceRequest(
report: OnyxTypes.Report,
@@ -1399,6 +1716,185 @@ function getUpdateMoneyRequestParams(
};
}
+/**
+ * @param transactionID
+ * @param transactionThreadReportID
+ * @param transactionChanges
+ * @param [transactionChanges.created] Present when updated the date field
+ * @param onlyIncludeChangedFields
+ * When 'true', then the returned params will only include the transaction details for the fields that were changed.
+ * When `false`, then the returned params will include all the transaction details, regardless of which fields were changed.
+ * This setting is necessary while the UpdateDistanceRequest API is refactored to be fully 1:1:1 in https://github.com/Expensify/App/issues/28358
+ */
+function getUpdateTrackExpenseParams(
+ transactionID: string,
+ transactionThreadReportID: string,
+ transactionChanges: TransactionChanges,
+ onlyIncludeChangedFields: boolean,
+): UpdateMoneyRequestData {
+ const optimisticData: OnyxUpdate[] = [];
+ const successData: OnyxUpdate[] = [];
+ const failureData: OnyxUpdate[] = [];
+
+ // Step 1: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData
+ const pendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE]));
+ const clearedPendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, null]));
+ const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}]));
+
+ // Step 2: Get all the collections being updated
+ const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
+ const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThread?.parentReportID}`] ?? null;
+ const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction);
+ let updatedTransaction = transaction ? TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, false) : null;
+ const transactionDetails = ReportUtils.getTransactionDetails(updatedTransaction);
+
+ if (transactionDetails?.waypoints) {
+ // This needs to be a JSON string since we're sending this to the MapBox API
+ transactionDetails.waypoints = JSON.stringify(transactionDetails.waypoints);
+ }
+
+ const dataToIncludeInParams: Partial | undefined = onlyIncludeChangedFields
+ ? Object.fromEntries(Object.entries(transactionDetails ?? {}).filter(([key]) => Object.keys(transactionChanges).includes(key)))
+ : transactionDetails;
+
+ const params: UpdateMoneyRequestParams = {
+ ...dataToIncludeInParams,
+ reportID: chatReport?.reportID,
+ transactionID,
+ };
+
+ const hasPendingWaypoints = 'waypoints' in transactionChanges;
+ if (transaction && updatedTransaction && hasPendingWaypoints) {
+ updatedTransaction = {
+ ...updatedTransaction,
+ amount: CONST.IOU.DEFAULT_AMOUNT,
+ modifiedAmount: CONST.IOU.DEFAULT_AMOUNT,
+ modifiedMerchant: Localize.translateLocal('iou.routePending'),
+ };
+
+ // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors
+ successData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`,
+ value: null,
+ });
+
+ // Revert the transaction's amount to the original value on failure.
+ // The IOU Report will be fully reverted in the failureData further below.
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {
+ amount: transaction.amount,
+ modifiedAmount: transaction.modifiedAmount,
+ modifiedMerchant: transaction.modifiedMerchant,
+ },
+ });
+ }
+
+ // Step 3: Build the modified expense report actions
+ // We don't create a modified report action if we're updating the waypoints,
+ // since there isn't actually any optimistic data we can create for them and the report action is created on the server
+ // with the response from the MapBox API
+ const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, false);
+ if (!hasPendingWaypoints) {
+ params.reportActionID = updatedReportAction.reportActionID;
+
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`,
+ value: {
+ [updatedReportAction.reportActionID]: updatedReportAction as OnyxTypes.ReportAction,
+ },
+ });
+ successData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`,
+ value: {
+ [updatedReportAction.reportActionID]: {pendingAction: null},
+ },
+ });
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`,
+ value: {
+ [updatedReportAction.reportActionID]: {
+ ...(updatedReportAction as OnyxTypes.ReportAction),
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericEditFailureMessage'),
+ },
+ },
+ });
+ }
+
+ // Step 4: Update the report preview message (and report header) so LHN amount tracked is correct.
+ // Optimistically modify the transaction and the transaction thread
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {
+ ...updatedTransaction,
+ pendingFields,
+ isLoading: hasPendingWaypoints,
+ errorFields: null,
+ },
+ });
+
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`,
+ value: {
+ lastActorAccountID: updatedReportAction.actorAccountID,
+ },
+ });
+
+ if (isScanning && ('amount' in transactionChanges || 'currency' in transactionChanges)) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ value: {
+ [transactionThread?.parentReportActionID ?? '']: {
+ whisperedToAccountIDs: [],
+ },
+ },
+ });
+ }
+
+ // Clear out the error fields and loading states on success
+ successData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {
+ pendingFields: clearedPendingFields,
+ isLoading: false,
+ errorFields: null,
+ },
+ });
+
+ // Clear out loading states, pending fields, and add the error fields
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {
+ pendingFields: clearedPendingFields,
+ isLoading: false,
+ errorFields,
+ },
+ });
+
+ // Reset the transaction thread to its original state
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`,
+ value: transactionThread,
+ });
+
+ return {
+ params,
+ onyxData: {optimisticData, successData, failureData},
+ };
+}
+
/** Updates the created date of a money request */
function updateMoneyRequestDate(
transactionID: string,
@@ -1411,7 +1907,14 @@ function updateMoneyRequestDate(
const transactionChanges: TransactionChanges = {
created: value,
};
- const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true);
+ const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
+ let data: UpdateMoneyRequestData;
+ if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) {
+ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true);
+ } else {
+ data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true);
+ }
+ const {params, onyxData} = data;
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData);
}
@@ -1443,7 +1946,14 @@ function updateMoneyRequestMerchant(
const transactionChanges: TransactionChanges = {
merchant: value,
};
- const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
+ const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
+ let data: UpdateMoneyRequestData;
+ if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) {
+ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true);
+ } else {
+ data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
+ }
+ const {params, onyxData} = data;
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT, params, onyxData);
}
@@ -1475,7 +1985,14 @@ function updateMoneyRequestDistance(
const transactionChanges: TransactionChanges = {
waypoints,
};
- const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
+ const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
+ let data: UpdateMoneyRequestData;
+ if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) {
+ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true);
+ } else {
+ data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
+ }
+ const {params, onyxData} = data;
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE, params, onyxData);
}
@@ -1507,7 +2024,14 @@ function updateMoneyRequestDescription(
const transactionChanges: TransactionChanges = {
comment,
};
- const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
+ const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
+ let data: UpdateMoneyRequestData;
+ if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) {
+ data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true);
+ } else {
+ data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true);
+ }
+ const {params, onyxData} = data;
API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION, params, onyxData);
}
@@ -1622,6 +2146,93 @@ function requestMoney(
Report.notifyNewAction(activeReportID, payeeAccountID);
}
+/**
+ * Track an expense
+ */
+function trackExpense(
+ report: OnyxTypes.Report,
+ amount: number,
+ currency: string,
+ created: string,
+ merchant: string,
+ payeeEmail: string,
+ payeeAccountID: number,
+ participant: Participant,
+ comment: string,
+ receipt: Receipt,
+ category?: string,
+ tag?: string,
+ taxCode = '',
+ taxAmount = 0,
+ billable?: boolean,
+ policy?: OnyxEntry,
+ policyTagList?: OnyxEntry,
+ policyCategories?: OnyxEntry,
+ gpsPoints = undefined,
+) {
+ const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created);
+ const {
+ iouReport,
+ chatReport,
+ transaction,
+ iouAction,
+ createdChatReportActionID,
+ createdIOUReportActionID,
+ reportPreviewAction,
+ transactionThreadReportID,
+ createdReportActionIDForThread,
+ onyxData,
+ } = getTrackExpenseInformation(
+ report,
+ participant,
+ comment,
+ amount,
+ currency,
+ currentCreated,
+ merchant,
+ receipt,
+ category,
+ tag,
+ billable,
+ policy,
+ policyTagList,
+ policyCategories,
+ payeeEmail,
+ );
+ const activeReportID = report.reportID;
+
+ const parameters: TrackExpenseParams = {
+ amount,
+ currency,
+ comment,
+ created: currentCreated,
+ merchant,
+ iouReportID: iouReport?.reportID,
+ chatReportID: chatReport.reportID,
+ transactionID: transaction.transactionID,
+ reportActionID: iouAction.reportActionID,
+ createdChatReportActionID,
+ createdIOUReportActionID,
+ reportPreviewReportActionID: reportPreviewAction?.reportActionID,
+ receipt,
+ receiptState: receipt?.state,
+ category,
+ tag,
+ taxCode,
+ taxAmount,
+ billable,
+ // This needs to be a string of JSON because of limitations with the fetch() API and nested objects
+ gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined,
+ transactionThreadReportID,
+ createdReportActionIDForThread,
+ };
+
+ API.write(WRITE_COMMANDS.TRACK_EXPENSE, parameters, onyxData);
+ resetMoneyRequestInfo();
+ Navigation.dismissModal(activeReportID);
+ Report.notifyNewAction(activeReportID, payeeAccountID);
+}
+
/**
* Build the Onyx data and IOU split necessary for splitting a bill with 3+ users.
* 1. Build the optimistic Onyx data for the group chat, i.e. chatReport and iouReportAction creating the former if it doesn't yet exist.
@@ -3238,6 +3849,167 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor
}
}
+function deleteTrackExpense(chatReportID: string, transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false) {
+ // STEP 1: Get all collections we're updating
+ const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null;
+ const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`];
+ const transactionThreadID = reportAction.childReportID;
+ let transactionThread = null;
+ if (transactionThreadID) {
+ transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`] ?? null;
+ }
+
+ // STEP 2: Decide if we need to:
+ // 1. Delete the transactionThread - delete if there are no visible comments in the thread
+ // 2. Update the moneyRequestPreview to show [Deleted request] - update if the transactionThread exists AND it isn't being deleted
+ const shouldDeleteTransactionThread = transactionThreadID ? (reportAction?.childVisibleActionCount ?? 0) === 0 : false;
+ const shouldShowDeletedRequestMessage = !!transactionThreadID && !shouldDeleteTransactionThread;
+
+ // STEP 3: Update the IOU reportAction.
+ const updatedReportAction = {
+ [reportAction.reportActionID]: {
+ pendingAction: shouldShowDeletedRequestMessage ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ previousMessage: reportAction.message,
+ message: [
+ {
+ type: 'COMMENT',
+ html: '',
+ text: '',
+ isEdited: true,
+ isDeletedParentAction: shouldShowDeletedRequestMessage,
+ },
+ ],
+ originalMessage: {
+ IOUTransactionID: null,
+ },
+ errors: undefined,
+ },
+ } as OnyxTypes.ReportActions;
+
+ const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(chatReport?.reportID ?? '', updatedReportAction);
+ const reportLastMessageText = ReportActionsUtils.getLastVisibleMessage(chatReport?.reportID ?? '', updatedReportAction).lastMessageText;
+
+ // STEP 4: Build Onyx data
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: null,
+ },
+ ];
+
+ if (Permissions.canUseViolations(betas)) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
+ value: null,
+ });
+ }
+
+ if (shouldDeleteTransactionThread) {
+ optimisticData.push(
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`,
+ value: null,
+ },
+ );
+ }
+
+ optimisticData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ value: updatedReportAction,
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`,
+ value: {
+ lastMessageText: reportLastMessageText,
+ lastVisibleActionCreated: lastVisibleAction?.created,
+ },
+ },
+ );
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ pendingAction: null,
+ errors: null,
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: transaction,
+ },
+ ];
+
+ if (Permissions.canUseViolations(betas)) {
+ failureData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
+ value: transactionViolations,
+ });
+ }
+
+ if (shouldDeleteTransactionThread) {
+ failureData.push({
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`,
+ value: transactionThread,
+ });
+ }
+
+ failureData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ ...reportAction,
+ pendingAction: null,
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericDeleteFailureMessage'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`,
+ value: chatReport,
+ },
+ );
+
+ const parameters: DeleteMoneyRequestParams = {
+ transactionID,
+ reportActionID: reportAction.reportActionID,
+ };
+
+ // STEP 6: Make the API request
+ API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData});
+ CachedPDFPaths.clearByKey(transactionID);
+
+ // STEP 7: Navigate the user depending on which page they are on and which resources were deleted
+ if (isSingleTransactionView && shouldDeleteTransactionThread) {
+ // Pop the deleted report screen before navigating. This prevents navigating to the Concierge chat due to the missing report.
+ Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(chatReport?.reportID ?? ''));
+ }
+}
+
/**
* @param managerID - Account ID of the person sending the money
* @param recipient - The user receiving the money
@@ -4160,9 +4932,11 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On
// If the report is iou or expense report, we should get the chat report to set participant for request money
const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report;
const currentUserAccountID = currentUserPersonalDetails.accountID;
- const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport)
- ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: true, selected: true}]
- : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true}));
+ const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport);
+ const participants: Participant[] =
+ ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport
+ ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}]
+ : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true}));
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true});
}
@@ -4385,6 +5159,7 @@ export {
setMoneyRequestParticipants,
createDistanceRequest,
deleteMoneyRequest,
+ deleteTrackExpense,
splitBill,
splitBillAndOpenReport,
setDraftSplitTransaction,
@@ -4440,6 +5215,7 @@ export {
cancelPayment,
navigateToStartStepIfScanFileCannotBeRead,
savePreferredPaymentMethod,
+ trackExpense,
canIOUBePaid,
canApproveIOU,
};
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index 1385a99fac0f..b4fe8b3e585f 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -252,6 +252,14 @@ function updateLastAccessedWorkspace(policyID: OnyxEntry) {
Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID);
}
+/**
+ * Checks if the currency is supported for direct reimbursement
+ * USD currency is the only one supported in NewDot for now
+ */
+function isCurrencySupportedForDirectReimbursement(currency: string) {
+ return currency === CONST.CURRENCY.USD;
+}
+
/**
* Check if the user has any active free policies (aka workspaces)
*/
@@ -471,6 +479,7 @@ function setWorkspaceAutoReporting(policyID: string, enabled: boolean, frequency
},
autoReportingFrequency: policy.autoReportingFrequency ?? null,
pendingFields: {autoReporting: null},
+ errorFields: {autoReporting: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.autoReportingErrorMessage')},
},
},
];
@@ -511,6 +520,7 @@ function setWorkspaceAutoReportingFrequency(policyID: string, frequency: ValueOf
value: {
autoReportingFrequency: policy.autoReportingFrequency ?? null,
pendingFields: {autoReportingFrequency: null},
+ errorFields: {autoReportingFrequency: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.autoReportingFrequencyErrorMessage')},
},
},
];
@@ -551,6 +561,7 @@ function setWorkspaceAutoReportingMonthlyOffset(policyID: string, autoReportingO
value: {
autoReportingOffset: policy.autoReportingOffset ?? null,
pendingFields: {autoReportingOffset: null},
+ errorFields: {autoReportingOffset: ErrorUtils.getMicroSecondOnyxError('workflowsDelayedSubmissionPage.monthlyOffsetErrorMessage')},
},
},
];
@@ -596,6 +607,7 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo
approver: policy.approver ?? null,
approvalMode: policy.approvalMode ?? null,
pendingFields: {approvalMode: null},
+ errorFields: {approvalMode: ErrorUtils.getMicroSecondOnyxError('workflowsApprovalPage.genericErrorMessage')},
},
},
];
@@ -666,8 +678,8 @@ function setWorkspacePayer(policyID: string, reimburserEmail: string, reimburser
API.write(WRITE_COMMANDS.SET_WORKSPACE_PAYER, params, {optimisticData, failureData, successData});
}
-function clearWorkspacePayerError(policyID: string) {
- Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {reimburserEmail: null}});
+function clearPolicyErrorField(policyID: string, fieldName: string) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {[fieldName]: null}});
}
function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserAccountID: number, reimburserEmail: string) {
@@ -1367,6 +1379,7 @@ function updateGeneralSettings(policyID: string, name: string, currency: string)
},
},
];
+
const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -3403,7 +3416,15 @@ function navigateWhenEnableFeature(policyID: string, featureRoute: Route) {
return;
}
- Navigation.navigate(featureRoute);
+ /**
+ * The app needs to set a navigation action to the microtask queue, it guarantees to execute Onyx.update first, then the navigation action.
+ * More details - https://github.com/Expensify/App/issues/37785#issuecomment-1989056726.
+ */
+ new Promise((resolve) => {
+ resolve();
+ }).then(() => {
+ Navigation.navigate(featureRoute);
+ });
}
function enablePolicyCategories(policyID: string, enabled: boolean) {
@@ -4297,7 +4318,6 @@ export {
renamePolicyCategory,
clearCategoryErrors,
setWorkspacePayer,
- clearWorkspacePayerError,
setWorkspaceReimbursement,
openPolicyWorkflowsPage,
setPolicyRequiresTag,
@@ -4326,5 +4346,7 @@ export {
setWorkspaceCurrencyDefault,
setForeignCurrencyDefault,
setPolicyCustomTaxName,
+ clearPolicyErrorField,
+ isCurrencySupportedForDirectReimbursement,
clearPolicyDistanceRatesErrorFields,
};
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 49ecfce36cf0..97dc3a6319d0 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -1589,29 +1589,30 @@ function updateReportName(reportID: string, value: string, previousValue: string
}
function updateReportField(reportID: string, reportField: PolicyReportField, previousReportField: PolicyReportField) {
- const recentlyUsedValues = allRecentlyUsedReportFields?.[reportField.fieldID] ?? [];
+ const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID);
+ const recentlyUsedValues = allRecentlyUsedReportFields?.[fieldKey] ?? [];
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {
- reportFields: {
- [reportField.fieldID]: reportField,
+ fieldList: {
+ [fieldKey]: reportField,
},
pendingFields: {
- [reportField.fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ [fieldKey]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
},
},
];
- if (reportField.type === 'dropdown') {
+ if (reportField.type === 'dropdown' && reportField.value) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS,
value: {
- [reportField.fieldID]: [...new Set([...recentlyUsedValues, reportField.value])],
+ [fieldKey]: [...new Set([...recentlyUsedValues, reportField.value])],
},
});
}
@@ -1621,14 +1622,14 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {
- reportFields: {
- [reportField.fieldID]: previousReportField,
+ fieldList: {
+ [fieldKey]: previousReportField,
},
pendingFields: {
- [reportField.fieldID]: null,
+ [fieldKey]: null,
},
errorFields: {
- [reportField.fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'),
+ [fieldKey]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'),
},
},
},
@@ -1639,7 +1640,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS,
value: {
- [reportField.fieldID]: recentlyUsedValues,
+ [fieldKey]: recentlyUsedValues,
},
});
}
@@ -1650,10 +1651,10 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {
pendingFields: {
- [reportField.fieldID]: null,
+ [fieldKey]: null,
},
errorFields: {
- [reportField.fieldID]: null,
+ [fieldKey]: null,
},
},
},
@@ -1661,7 +1662,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre
const parameters = {
reportID,
- reportFields: JSON.stringify({[`expensify_${reportField.fieldID}`]: reportField}),
+ reportFields: JSON.stringify({[fieldKey]: reportField}),
};
API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData});
diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts
index 1bad1de0a9f5..3f2420c76f87 100644
--- a/src/libs/actions/TaxRate.ts
+++ b/src/libs/actions/TaxRate.ts
@@ -1,14 +1,25 @@
+import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
+import type {FormOnyxValues} from '@components/Form/types';
import * as API from '@libs/API';
-import type {CreatePolicyTaxParams} from '@libs/API/parameters';
+import type {CreatePolicyTaxParams, DeletePolicyTaxesParams, RenamePolicyTaxParams, SetPolicyTaxesEnabledParams, UpdatePolicyTaxValueParams} from '@libs/API/parameters';
import {WRITE_COMMANDS} from '@libs/API/types';
+import * as ValidationUtils from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import * as ErrorUtils from '@src/libs/ErrorUtils';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {TaxRate, TaxRates} from '@src/types/onyx';
-import type {PendingAction} from '@src/types/onyx/OnyxCommon';
+import INPUT_IDS from '@src/types/form/WorkspaceNewTaxForm';
+import type {Policy, TaxRate, TaxRates} from '@src/types/onyx';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {OnyxData} from '@src/types/onyx/Request';
+let allPolicies: OnyxCollection;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.POLICY,
+ waitForCollectionCallback: true,
+ callback: (value) => (allPolicies = value),
+});
+
/**
* Get tax value with percentage
*/
@@ -20,6 +31,34 @@ function covertTaxNameToID(name: string) {
return `id_${name.toUpperCase().replaceAll(' ', '_')}`;
}
+/**
+ * Function to validate tax name
+ */
+const validateTaxName = (policy: Policy, values: FormOnyxValues) => {
+ const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.NAME]);
+
+ const name = values[INPUT_IDS.NAME];
+ if (policy?.taxRates?.taxes && ValidationUtils.isExistingTaxName(name, policy.taxRates.taxes)) {
+ errors[INPUT_IDS.NAME] = 'workspace.taxes.errors.taxRateAlreadyExists';
+ }
+
+ return errors;
+};
+
+/**
+ * Function to validate tax value
+ */
+const validateTaxValue = (values: FormOnyxValues) => {
+ const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.VALUE]);
+
+ const value = values[INPUT_IDS.VALUE];
+ if (!ValidationUtils.isValidPercentage(value)) {
+ errors[INPUT_IDS.VALUE] = 'workspace.taxes.errors.valuePercentageRange';
+ }
+
+ return errors;
+};
+
/**
* Get new tax ID
*/
@@ -39,7 +78,8 @@ function getNextTaxCode(name: string, taxRates?: TaxRates): string {
function createPolicyTax(policyID: string, taxRate: TaxRate) {
if (!taxRate.code) {
- throw new Error('Tax code is required when creating a new tax rate.');
+ console.debug('Policy or tax rates not found');
+ return;
}
const onyxData: OnyxData = {
@@ -83,7 +123,7 @@ function createPolicyTax(policyID: string, taxRate: TaxRate) {
taxRates: {
taxes: {
[taxRate.code]: {
- errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.genericFailureMessage'),
+ errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.createFailureMessage'),
},
},
},
@@ -105,7 +145,24 @@ function createPolicyTax(policyID: string, taxRate: TaxRate) {
API.write(WRITE_COMMANDS.CREATE_POLICY_TAX, parameters, onyxData);
}
-function clearTaxRateError(policyID: string, taxID: string, pendingAction?: PendingAction) {
+function clearTaxRateFieldError(policyID: string, taxID: string, field: keyof TaxRate) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ taxRates: {
+ taxes: {
+ [taxID]: {
+ pendingFields: {
+ [field]: null,
+ },
+ errorFields: {
+ [field]: null,
+ },
+ },
+ },
+ },
+ });
+}
+
+function clearTaxRateError(policyID: string, taxID: string, pendingAction?: OnyxCommon.PendingAction) {
if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
taxRates: {
@@ -119,10 +176,288 @@ function clearTaxRateError(policyID: string, taxID: string, pendingAction?: Pend
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
taxRates: {
taxes: {
- [taxID]: {pendingAction: null, errors: null},
+ [taxID]: {pendingAction: null, errors: null, errorFields: null},
},
},
});
}
-export {createPolicyTax, clearTaxRateError, getNextTaxCode, getTaxValueWithPercentage};
+type TaxRateEnabledMap = Record>;
+
+function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isEnabled: boolean) {
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
+ const originalTaxes = {...policy?.taxRates?.taxes};
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: taxesIDsToUpdate.reduce((acc, taxID) => {
+ acc[taxID] = {
+ isDisabled: !isEnabled,
+ pendingFields: {isDisabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ errorFields: {isDisabled: null},
+ };
+ return acc;
+ }, {}),
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: taxesIDsToUpdate.reduce((acc, taxID) => {
+ acc[taxID] = {pendingFields: {isDisabled: null}, errorFields: {isDisabled: null}};
+ return acc;
+ }, {}),
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: taxesIDsToUpdate.reduce((acc, taxID) => {
+ acc[taxID] = {
+ isDisabled: !!originalTaxes[taxID].isDisabled,
+ pendingFields: {isDisabled: null},
+ errorFields: {isDisabled: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.updateFailureMessage')},
+ };
+ return acc;
+ }, {}),
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ taxFieldsArray: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: taxID, enabled: isEnabled}))),
+ } satisfies SetPolicyTaxesEnabledParams;
+
+ API.write(WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED, parameters, onyxData);
+}
+
+type TaxRateDeleteMap = Record<
+ string,
+ | (Pick & {
+ errors: OnyxCommon.Errors | null;
+ })
+ | null
+>;
+
+function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) {
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
+ const policyTaxRates = policy?.taxRates?.taxes;
+
+ if (!policyTaxRates) {
+ console.debug('Policy or tax rates not found');
+ return;
+ }
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: taxesToDelete.reduce((acc, taxID) => {
+ acc[taxID] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, errors: null};
+ return acc;
+ }, {}),
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: taxesToDelete.reduce((acc, taxID) => {
+ acc[taxID] = null;
+ return acc;
+ }, {}),
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: taxesToDelete.reduce((acc, taxID) => {
+ acc[taxID] = {
+ pendingAction: null,
+ errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.deleteFailureMessage'),
+ };
+ return acc;
+ }, {}),
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ taxNames: JSON.stringify(taxesToDelete.map((taxID) => policyTaxRates[taxID].name)),
+ } satisfies DeletePolicyTaxesParams;
+
+ API.write(WRITE_COMMANDS.DELETE_POLICY_TAXES, parameters, onyxData);
+}
+
+function updatePolicyTaxValue(policyID: string, taxID: string, taxValue: number) {
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
+ const originalTaxRate = {...policy?.taxRates?.taxes[taxID]};
+ const stringTaxValue = `${taxValue}%`;
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: {
+ [taxID]: {
+ value: stringTaxValue,
+ pendingFields: {value: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ errorFields: {value: null},
+ },
+ },
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: {
+ [taxID]: {pendingFields: {value: null}, errorFields: {value: null}},
+ },
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: {
+ [taxID]: {
+ value: originalTaxRate.value,
+ pendingFields: {value: null},
+ errorFields: {value: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.updateFailureMessage')},
+ },
+ },
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ taxCode: taxID,
+ taxAmount: Number(taxValue),
+ } satisfies UpdatePolicyTaxValueParams;
+
+ API.write(WRITE_COMMANDS.UPDATE_POLICY_TAX_VALUE, parameters, onyxData);
+}
+
+function renamePolicyTax(policyID: string, taxID: string, newName: string) {
+ const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
+ const originalTaxRate = {...policy?.taxRates?.taxes[taxID]};
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: {
+ [taxID]: {
+ name: newName,
+ pendingFields: {name: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE},
+ errorFields: {name: null},
+ },
+ },
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: {
+ [taxID]: {pendingFields: {name: null}, errorFields: {name: null}},
+ },
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ taxRates: {
+ taxes: {
+ [taxID]: {
+ name: originalTaxRate.name,
+ pendingFields: {name: null},
+ errorFields: {name: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.errors.updateFailureMessage')},
+ },
+ },
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ taxCode: taxID,
+ newName,
+ } satisfies RenamePolicyTaxParams;
+
+ API.write(WRITE_COMMANDS.RENAME_POLICY_TAX, parameters, onyxData);
+}
+
+export {
+ createPolicyTax,
+ getNextTaxCode,
+ clearTaxRateError,
+ clearTaxRateFieldError,
+ getTaxValueWithPercentage,
+ setPolicyTaxesEnabled,
+ validateTaxName,
+ validateTaxValue,
+ deletePolicyTaxes,
+ updatePolicyTaxValue,
+ renamePolicyTax,
+};
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 6655d78cb0a8..2d23edfba93f 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -30,6 +30,7 @@ import PusherUtils from '@libs/PusherUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import playSound, {SOUNDS} from '@libs/Sound';
import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile';
+import Visibility from '@libs/Visibility';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -489,80 +490,84 @@ const isChannelMuted = (reportId: string) =>
function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) {
const reportActionsOnly = pushJSON.filter((update) => update.key?.includes('reportActions_'));
// "reportActions_5134363522480668" -> "5134363522480668"
- const reportIDs = reportActionsOnly.map((value) => value.key.split('_')[1]);
+ const reportID = reportActionsOnly
+ .map((value) => value.key.split('_')[1])
+ .find((reportKey) => reportKey === Navigation.getTopmostReportId() && Visibility.isVisible() && Visibility.hasFocus());
- Promise.all(reportIDs.map((reportID) => isChannelMuted(reportID)))
- .then((muted) => muted.every((isMuted) => isMuted))
- .then((isSoundMuted) => {
- if (isSoundMuted) {
- return;
+ if (!reportID) {
+ return;
+ }
+
+ isChannelMuted(reportID).then((isSoundMuted) => {
+ if (isSoundMuted) {
+ return;
+ }
+
+ try {
+ const flatten = reportActionsOnly.flatMap((update) => {
+ const value = update.value as OnyxCollection;
+
+ if (!value) {
+ return [];
+ }
+
+ return Object.values(value);
+ }) as ReportAction[];
+
+ for (const data of flatten) {
+ // Someone completes a task
+ if (data.actionName === 'TASKCOMPLETED') {
+ return playSound(SOUNDS.SUCCESS);
+ }
}
- try {
- const flatten = reportActionsOnly.flatMap((update) => {
- const value = update.value as OnyxCollection;
+ const types = flatten.map((data) => data?.originalMessage).filter(Boolean) as OriginalMessage[];
- if (!value) {
- return [];
- }
+ for (const message of types) {
+ // someone sent money
+ if ('IOUDetails' in message) {
+ return playSound(SOUNDS.SUCCESS);
+ }
- return Object.values(value);
- }) as ReportAction[];
+ // mention user
+ if ('html' in message && typeof message.html === 'string' && message.html.includes(`@${currentEmail}`)) {
+ return playSoundExcludingMobile(SOUNDS.ATTENTION);
+ }
+
+ // mention @here
+ if ('html' in message && typeof message.html === 'string' && message.html.includes('')) {
+ return playSoundExcludingMobile(SOUNDS.ATTENTION);
+ }
- for (const data of flatten) {
- // Someone completes a task
- if (data.actionName === 'TASKCOMPLETED') {
- return playSound(SOUNDS.SUCCESS);
- }
+ // assign a task
+ if ('taskReportID' in message) {
+ return playSound(SOUNDS.ATTENTION);
}
- const types = flatten.map((data) => data?.originalMessage).filter(Boolean) as OriginalMessage[];
-
- for (const message of types) {
- // someone sent money
- if ('IOUDetails' in message) {
- return playSound(SOUNDS.SUCCESS);
- }
-
- // mention user
- if ('html' in message && typeof message.html === 'string' && message.html.includes(`@${currentEmail}`)) {
- return playSoundExcludingMobile(SOUNDS.ATTENTION);
- }
-
- // mention @here
- if ('html' in message && typeof message.html === 'string' && message.html.includes('')) {
- return playSoundExcludingMobile(SOUNDS.ATTENTION);
- }
-
- // assign a task
- if ('taskReportID' in message) {
- return playSound(SOUNDS.ATTENTION);
- }
-
- // request money
- if ('IOUTransactionID' in message) {
- return playSound(SOUNDS.ATTENTION);
- }
-
- // Someone completes a money request
- if ('IOUReportID' in message) {
- return playSound(SOUNDS.SUCCESS);
- }
-
- // plain message
- if ('html' in message) {
- return playSoundExcludingMobile(SOUNDS.RECEIVE);
- }
+ // request money
+ if ('IOUTransactionID' in message) {
+ return playSound(SOUNDS.ATTENTION);
}
- } catch (e) {
- let errorMessage = String(e);
- if (e instanceof Error) {
- errorMessage = e.message;
+
+ // Someone completes a money request
+ if ('IOUReportID' in message) {
+ return playSound(SOUNDS.SUCCESS);
}
- Log.client(`Unexpected error occurred while parsing the data to play a sound: ${errorMessage}`);
+ // plain message
+ if ('html' in message) {
+ return playSoundExcludingMobile(SOUNDS.RECEIVE);
+ }
}
- });
+ } catch (e) {
+ let errorMessage = String(e);
+ if (e instanceof Error) {
+ errorMessage = e.message;
+ }
+
+ Log.client(`Unexpected error occurred while parsing the data to play a sound: ${errorMessage}`);
+ }
+ });
}
/**
@@ -960,11 +965,9 @@ function dismissReferralBanner(type: ValueOf we're either logged-in or shown 2FA screen
+ // !isSignedIn - confirms we're not signed-in yet as there's possible one last step (2FA validation)
+ const shouldPopToTop = (autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN) && !isSignedIn;
+
+ if (shouldPopToTop) {
+ Navigation.isNavigationReady().then(() => Navigation.resetToHome());
+ }
+}
+
+export default desktopLoginRedirect;
diff --git a/src/libs/desktopLoginRedirect/index.ts b/src/libs/desktopLoginRedirect/index.ts
new file mode 100644
index 000000000000..14f5750c3de9
--- /dev/null
+++ b/src/libs/desktopLoginRedirect/index.ts
@@ -0,0 +1,5 @@
+import type {AutoAuthState} from '@src/types/onyx/Session';
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+function desktopLoginRedirect(autoAuthState: AutoAuthState, isSignedIn: boolean) {}
+export default desktopLoginRedirect;
diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts
index 1202275067a5..5ce899cdd316 100644
--- a/src/libs/migrateOnyx.ts
+++ b/src/libs/migrateOnyx.ts
@@ -1,5 +1,6 @@
import Log from './Log';
import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID';
+import NVPMigration from './migrations/NVPMigration';
import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts';
import RenameReceiptFilename from './migrations/RenameReceiptFilename';
import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection';
@@ -10,7 +11,7 @@ export default function (): Promise {
return new Promise((resolve) => {
// Add all migrations to an array so they are executed in order
- const migrationPromises = [RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts];
+ const migrationPromises = [RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts, NVPMigration];
// Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the
// previous promise to finish before moving onto the next one.
diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts
new file mode 100644
index 000000000000..9ab774328f78
--- /dev/null
+++ b/src/libs/migrations/NVPMigration.ts
@@ -0,0 +1,86 @@
+import after from 'lodash/after';
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+// These are the oldKeyName: newKeyName of the NVPs we can migrate without any processing
+const migrations = {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ nvp_lastPaymentMethod: ONYXKEYS.NVP_LAST_PAYMENT_METHOD,
+ isFirstTimeNewExpensifyUser: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
+ preferredLocale: ONYXKEYS.NVP_PREFERRED_LOCALE,
+ preferredEmojiSkinTone: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
+ frequentlyUsedEmojis: ONYXKEYS.FREQUENTLY_USED_EMOJIS,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ private_blockedFromConcierge: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ private_pushNotificationID: ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID,
+ tryFocusMode: ONYXKEYS.NVP_TRY_FOCUS_MODE,
+ introSelected: ONYXKEYS.NVP_INTRO_SELECTED,
+ hasDismissedIdlePanel: ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL,
+};
+
+// This migration changes the keys of all the NVP related keys so that they are standardized
+export default function () {
+ return new Promise((resolve) => {
+ // Resolve the migration when all the keys have been migrated. The number of keys is the size of the `migrations` object in addition to the ACCOUNT and OLD_POLICY_RECENTLY_USED_TAGS keys (which is why there is a +2).
+ const resolveWhenDone = after(Object.entries(migrations).length + 2, () => resolve());
+
+ for (const [oldKey, newKey] of Object.entries(migrations)) {
+ const connectionID = Onyx.connect({
+ // @ts-expect-error oldKey is a variable
+ key: oldKey,
+ callback: (value) => {
+ Onyx.disconnect(connectionID);
+ if (value === null) {
+ resolveWhenDone();
+ return;
+ }
+ // @ts-expect-error These keys are variables, so we can't check the type
+ Onyx.multiSet({
+ [newKey]: value,
+ [oldKey]: null,
+ }).then(resolveWhenDone);
+ },
+ });
+ }
+ const connectionIDAccount = Onyx.connect({
+ key: ONYXKEYS.ACCOUNT,
+ callback: (value) => {
+ Onyx.disconnect(connectionIDAccount);
+ // @ts-expect-error we are removing this property, so it is not in the type anymore
+ if (!value?.activePolicyID) {
+ resolveWhenDone();
+ return;
+ }
+ // @ts-expect-error we are removing this property, so it is not in the type anymore
+ const activePolicyID = value.activePolicyID;
+ const newValue = {...value};
+ // @ts-expect-error we are removing this property, so it is not in the type anymore
+ delete newValue.activePolicyID;
+ Onyx.multiSet({
+ [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID,
+ [ONYXKEYS.ACCOUNT]: newValue,
+ }).then(resolveWhenDone);
+ },
+ });
+ const connectionIDRecentlyUsedTags = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS,
+ waitForCollectionCallback: true,
+ callback: (value) => {
+ Onyx.disconnect(connectionIDRecentlyUsedTags);
+ if (!value) {
+ resolveWhenDone();
+ return;
+ }
+ const newValue = {};
+ for (const key of Object.keys(value)) {
+ // @ts-expect-error We have no fixed types here
+ newValue[`nvp_${key}`] = value[key];
+ // @ts-expect-error We have no fixed types here
+ newValue[key] = null;
+ }
+ Onyx.multiSet(newValue).then(resolveWhenDone);
+ },
+ });
+ });
+}
diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx
index 45d2f31073ec..3d60884d3cfc 100644
--- a/src/pages/EditReportFieldDatePage.tsx
+++ b/src/pages/EditReportFieldDatePage.tsx
@@ -19,8 +19,8 @@ type EditReportFieldDatePageProps = {
/** Name of the policy report field */
fieldName: string;
- /** ID of the policy report field */
- fieldID: string;
+ /** Key of the policy report field */
+ fieldKey: string;
/** Flag to indicate if the field can be left blank */
isRequired: boolean;
@@ -29,7 +29,7 @@ type EditReportFieldDatePageProps = {
onSubmit: (form: FormOnyxValues) => void;
};
-function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) {
+function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldKey}: EditReportFieldDatePageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const inputRef = useRef(null);
@@ -37,12 +37,12 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f
const validate = useCallback(
(value: FormOnyxValues) => {
const errors: FormInputErrors = {};
- if (isRequired && value[fieldID].trim() === '') {
- errors[fieldID] = 'common.error.fieldRequired';
+ if (isRequired && value[fieldKey].trim() === '') {
+ errors[fieldKey] = 'common.error.fieldRequired';
}
return errors;
},
- [fieldID, isRequired],
+ [fieldKey, isRequired],
);
return (
@@ -67,8 +67,8 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f
{/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */}
InputComponent={DatePicker}
- inputID={fieldID}
- name={fieldID}
+ inputID={fieldKey}
+ name={fieldKey}
defaultValue={fieldValue}
label={fieldName}
accessibilityLabel={fieldName}
diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx
index 1ad3c766221b..a314120fb0c6 100644
--- a/src/pages/EditReportFieldDropdownPage.tsx
+++ b/src/pages/EditReportFieldDropdownPage.tsx
@@ -17,8 +17,8 @@ type EditReportFieldDropdownPageComponentProps = {
/** Name of the policy report field */
fieldName: string;
- /** ID of the policy report field */
- fieldID: string;
+ /** Key of the policy report field */
+ fieldKey: string;
/** ID of the policy this report field belongs to */
// eslint-disable-next-line react/no-unused-prop-types
@@ -37,12 +37,12 @@ type EditReportFieldDropdownPageOnyxProps = {
type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps;
-function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) {
+function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) {
const [searchValue, setSearchValue] = useState('');
const styles = useThemeStyles();
const {getSafeAreaMargins} = useStyleUtils();
const {translate} = useLocalize();
- const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldID] ?? [], [recentlyUsedReportFields, fieldID]);
+ const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldKey] ?? [], [recentlyUsedReportFields, fieldKey]);
const [headerMessage, setHeaderMessage] = useState('');
const sections = useMemo(() => {
@@ -93,7 +93,11 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue,
boldStyle
sections={sections}
value={searchValue}
- onSelectRow={(option: Record) => onSubmit({[fieldID]: option.text})}
+ onSelectRow={(option: Record) =>
+ onSubmit({
+ [fieldKey]: fieldValue === option.text ? '' : option.text,
+ })
+ }
onChangeText={setSearchValue}
highlightSelectedOptions
isRowMultilineSupported
diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx
index 4124a9ebef98..8c8376468c0f 100644
--- a/src/pages/EditReportFieldPage.tsx
+++ b/src/pages/EditReportFieldPage.tsx
@@ -9,7 +9,7 @@ import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
import * as ReportActions from '@src/libs/actions/Report';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Policy, PolicyReportFields, Report} from '@src/types/onyx';
+import type {Policy, Report} from '@src/types/onyx';
import EditReportFieldDatePage from './EditReportFieldDatePage';
import EditReportFieldDropdownPage from './EditReportFieldDropdownPage';
import EditReportFieldTextPage from './EditReportFieldTextPage';
@@ -18,9 +18,6 @@ type EditReportFieldPageOnyxProps = {
/** The report object for the expense report */
report: OnyxEntry;
- /** Policy report fields */
- policyReportFields: OnyxEntry;
-
/** Policy to which the report belongs to */
policy: OnyxEntry;
};
@@ -42,8 +39,9 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & {
};
};
-function EditReportFieldPage({route, policy, report, policyReportFields}: EditReportFieldPageProps) {
- const reportField = report?.reportFields?.[route.params.fieldID] ?? policyReportFields?.[route.params.fieldID];
+function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) {
+ const fieldKey = ReportUtils.getReportFieldKey(route.params.fieldID);
+ const reportField = report?.fieldList?.[fieldKey] ?? policy?.fieldList?.[fieldKey];
const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy);
if (!reportField || !report || isDisabled) {
@@ -65,11 +63,11 @@ function EditReportFieldPage({route, policy, report, policyReportFields}: EditRe
const isReportFieldTitle = ReportUtils.isReportFieldOfTypeTitle(reportField);
const handleReportFieldChange = (form: FormOnyxValues) => {
- const value = form[reportField.fieldID] || '';
+ const value = form[fieldKey];
if (isReportFieldTitle) {
ReportActions.updateReportName(report.reportID, value, report.reportName ?? '');
} else {
- ReportActions.updateReportField(report.reportID, {...reportField, value}, reportField);
+ ReportActions.updateReportField(report.reportID, {...reportField, value: value === '' ? null : value}, reportField);
}
Navigation.dismissModal(report?.reportID);
@@ -81,7 +79,7 @@ function EditReportFieldPage({route, policy, report, policyReportFields}: EditRe
return (
!(value in reportField.disabledOptions))}
onSubmit={handleReportFieldChange}
/>
);
@@ -121,9 +119,6 @@ export default withOnyx(
report: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`,
},
- policyReportFields: {
- key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${route.params.policyID}`,
- },
policy: {
key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`,
},
diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx
index 9cda559280a9..1a6cf96fb37a 100644
--- a/src/pages/EditReportFieldTextPage.tsx
+++ b/src/pages/EditReportFieldTextPage.tsx
@@ -19,8 +19,8 @@ type EditReportFieldTextPageProps = {
/** Name of the policy report field */
fieldName: string;
- /** ID of the policy report field */
- fieldID: string;
+ /** Key of the policy report field */
+ fieldKey: string;
/** Flag to indicate if the field can be left blank */
isRequired: boolean;
@@ -29,7 +29,7 @@ type EditReportFieldTextPageProps = {
onSubmit: (form: FormOnyxValues) => void;
};
-function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID}: EditReportFieldTextPageProps) {
+function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldKey}: EditReportFieldTextPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const inputRef = useRef(null);
@@ -37,12 +37,12 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f
const validate = useCallback(
(values: FormOnyxValues) => {
const errors: FormInputErrors = {};
- if (isRequired && values[fieldID].trim() === '') {
- errors[fieldID] = 'common.error.fieldRequired';
+ if (isRequired && values[fieldKey].trim() === '') {
+ errors[fieldKey] = 'common.error.fieldRequired';
}
return errors;
},
- [fieldID, isRequired],
+ [fieldKey, isRequired],
);
return (
@@ -66,8 +66,8 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f
;
/** An object that holds data about which referral banners have been dismissed */
- dismissedReferralBanners: DismissedReferralBanners;
+ dismissedReferralBanners: OnyxEntry;
/** Whether we are searching for reports in the server */
isSearchingForReports: OnyxEntry;
@@ -265,7 +264,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
shouldShowOptions={isOptionsDataReady && didScreenTransitionEnd}
shouldShowConfirmButton
- shouldShowReferralCTA={!dismissedReferralBanners[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]}
+ shouldShowReferralCTA={!dismissedReferralBanners?.[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]}
referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT}
confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')}
textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''}
@@ -287,8 +286,7 @@ NewChatPage.displayName = 'NewChatPage';
export default withOnyx({
dismissedReferralBanners: {
- key: ONYXKEYS.ACCOUNT,
- selector: (data) => data?.dismissedReferralBanners ?? {},
+ key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
},
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
diff --git a/src/pages/ValidateLoginPage/index.website.tsx b/src/pages/ValidateLoginPage/index.website.tsx
index 2acad7815754..b8e8709215e8 100644
--- a/src/pages/ValidateLoginPage/index.website.tsx
+++ b/src/pages/ValidateLoginPage/index.website.tsx
@@ -4,6 +4,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import ExpiredValidateCodeModal from '@components/ValidateCode/ExpiredValidateCodeModal';
import JustSignedInModal from '@components/ValidateCode/JustSignedInModal';
import ValidateCodeModal from '@components/ValidateCode/ValidateCodeModal';
+import desktopLoginRedirect from '@libs/desktopLoginRedirect';
import Navigation from '@libs/Navigation/Navigation';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
@@ -43,6 +44,11 @@ function ValidateLoginPage({
// The user has initiated the sign in process on the same browser, in another tab.
Session.signInWithValidateCode(Number(accountID), validateCode);
+
+ // Since on Desktop we don't have multi-tab functionality to handle the login flow,
+ // we need to `popToTop` the stack after `signInWithValidateCode` in order to
+ // perform login for both 2FA and non-2FA accounts.
+ desktopLoginRedirect(autoAuthState, isSignedIn);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index 009d55e6d139..e22b7c6cf1bc 100644
--- a/src/pages/home/ReportScreen.tsx
+++ b/src/pages/home/ReportScreen.tsx
@@ -171,7 +171,7 @@ function ReportScreen({
managerID: reportProp?.managerID,
total: reportProp?.total,
nonReimbursableTotal: reportProp?.nonReimbursableTotal,
- reportFields: reportProp?.reportFields,
+ fieldList: reportProp?.fieldList,
ownerAccountID: reportProp?.ownerAccountID,
currency: reportProp?.currency,
participantAccountIDs: reportProp?.participantAccountIDs,
@@ -208,7 +208,7 @@ function ReportScreen({
reportProp?.managerID,
reportProp?.total,
reportProp?.nonReimbursableTotal,
- reportProp?.reportFields,
+ reportProp?.fieldList,
reportProp?.ownerAccountID,
reportProp?.currency,
reportProp?.participantAccountIDs,
@@ -255,7 +255,7 @@ function ReportScreen({
: null,
[reportActions, parentReportAction],
);
- const isSingleTransactionView = ReportUtils.isMoneyRequest(report);
+ const isSingleTransactionView = ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report);
const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] ?? null;
const isTopMostReportId = currentReportID === getReportID(route);
const didSubscribeToReportLeavingEvents = useRef(false);
diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
index 793fbf9b1e7e..9bf32bb92b35 100644
--- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx
@@ -12,7 +12,6 @@ import calculateAnchorPosition from '@libs/calculateAnchorPosition';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as IOU from '@userActions/IOU';
import * as Report from '@userActions/Report';
-import CONST from '@src/CONST';
import type {ReportAction} from '@src/types/onyx';
import BaseReportActionContextMenu from './BaseReportActionContextMenu';
import type {ContextMenuAction} from './ContextMenuActions';
@@ -256,8 +255,12 @@ function PopoverReportActionContextMenu(_props: unknown, ref: ForwardedRef {
callbackWhenDeleteModalHide.current = () => (onComfirmDeleteModal.current = runAndResetCallback(onComfirmDeleteModal.current));
const reportAction = reportActionRef.current;
- if (ReportActionsUtils.isMoneyRequestAction(reportAction) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) {
- IOU.deleteMoneyRequest(reportAction?.originalMessage?.IOUTransactionID ?? '', reportAction);
+ if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
+ if (ReportActionsUtils.isTrackExpenseAction(reportAction)) {
+ IOU.deleteTrackExpense(reportIDRef.current, reportAction?.originalMessage?.IOUTransactionID ?? '', reportAction);
+ } else {
+ IOU.deleteMoneyRequest(reportAction?.originalMessage?.IOUTransactionID ?? '', reportAction);
+ }
} else if (reportAction) {
Report.deleteReportComment(reportIDRef.current, reportAction);
}
diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
index 6a21845f47ad..95533db02f06 100644
--- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
+++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx
@@ -13,6 +13,7 @@ import PopoverMenu from '@components/PopoverMenu';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -115,6 +116,7 @@ function AttachmentPickerWithMenuItems({
const styles = useThemeStyles();
const {translate} = useLocalize();
const {windowHeight} = useWindowDimensions();
+ const {canUseTrackExpense} = usePermissions();
/**
* Returns the list of IOU Options
@@ -136,12 +138,17 @@ function AttachmentPickerWithMenuItems({
text: translate('iou.sendMoney'),
onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID ?? ''),
},
+ [CONST.IOU.TYPE.TRACK_EXPENSE]: {
+ icon: Expensicons.DocumentPlus,
+ text: translate('iou.trackExpense'),
+ onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''),
+ },
};
- return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => ({
+ return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({
...options[option],
}));
- }, [report, policy, reportParticipantIDs, translate]);
+ }, [translate, report, policy, reportParticipantIDs, canUseTrackExpense]);
/**
* Determines if we can show the task option
diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx
index f0280b27efa0..08a7836f928d 100644
--- a/src/pages/home/report/ReportActionItem.tsx
+++ b/src/pages/home/report/ReportActionItem.tsx
@@ -58,6 +58,7 @@ import * as ReportActions from '@userActions/ReportActions';
import * as Session from '@userActions/Session';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
@@ -97,9 +98,6 @@ type ReportActionItemOnyxProps = {
/** The user's wallet account */
userWallet: OnyxEntry;
- /** All policy report fields */
- policyReportFields: OnyxEntry;
-
/** The policy which the user has access to and which the report is tied to */
policy: OnyxEntry;
};
@@ -156,7 +154,6 @@ function ReportActionItem({
userWallet,
shouldHideThreadDividerLine = false,
shouldShowSubscriptAvatar = false,
- policyReportFields,
policy,
onPress = undefined,
}: ReportActionItemProps) {
@@ -414,7 +411,10 @@ function ReportActionItem({
isIOUReport(action) &&
action.originalMessage &&
// For the pay flow, we only want to show MoneyRequestAction when sending money. When paying, we display a regular system message
- (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT || isSendingMoney)
+ (action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE ||
+ action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT ||
+ action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ||
+ isSendingMoney)
) {
// There is no single iouReport for bill splits, so only 1:1 requests require an iouReportID
const iouReportID = action.originalMessage.IOUReportID ? action.originalMessage.IOUReportID.toString() : '0';
@@ -667,6 +667,14 @@ function ReportActionItem({
if (ReportActionsUtils.isTransactionThread(parentReportAction)) {
const isReversedTransaction = ReportActionsUtils.isReversedTransaction(parentReportAction);
if (ReportActionsUtils.isDeletedParentAction(parentReportAction) || isReversedTransaction) {
+ let message: TranslationPaths;
+ if (isReversedTransaction) {
+ message = 'parentReportAction.reversedTransaction';
+ } else if (ReportActionsUtils.isTrackExpenseAction(parentReportAction)) {
+ message = 'parentReportAction.deletedExpense';
+ } else {
+ message = 'parentReportAction.deletedRequest';
+ }
return (
@@ -677,9 +685,7 @@ function ReportActionItem({
showHeader
report={report}
>
- ${translate(isReversedTransaction ? 'parentReportAction.reversedTransaction' : 'parentReportAction.deletedRequest')}`}
- />
+ ${translate(message)}`} />
@@ -734,7 +740,6 @@ function ReportActionItem({
@@ -881,10 +886,6 @@ export default withOnyx({
},
initialValue: {} as OnyxTypes.Report,
},
- policyReportFields: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_REPORT_FIELDS}${report.policyID ?? 0}`,
- initialValue: {},
- },
policy: {
key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report.policyID ?? 0}`,
initialValue: {} as OnyxTypes.Policy,
@@ -925,8 +926,7 @@ export default withOnyx({
prevProps.report?.total === nextProps.report?.total &&
prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal &&
prevProps.linkedReportActionID === nextProps.linkedReportActionID &&
- lodashIsEqual(prevProps.policyReportFields, nextProps.policyReportFields) &&
- lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields) &&
+ lodashIsEqual(prevProps.report.fieldList, nextProps.report.fieldList) &&
lodashIsEqual(prevProps.policy, nextProps.policy) &&
lodashIsEqual(prevParentReportAction, nextParentReportAction)
);
diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
index ec27112ab4b7..abf932eff96d 100644
--- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
+++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js
@@ -9,6 +9,7 @@ import withNavigation from '@components/withNavigation';
import withNavigationFocus from '@components/withNavigationFocus';
import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
@@ -75,6 +76,7 @@ function FloatingActionButtonAndPopover(props) {
const {translate} = useLocalize();
const [isCreateMenuActive, setIsCreateMenuActive] = useState(false);
const fabRef = useRef(null);
+ const {canUseTrackExpense} = usePermissions();
const prevIsFocused = usePrevious(props.isFocused);
@@ -187,13 +189,28 @@ function FloatingActionButtonAndPopover(props) {
),
),
},
- ...[
- {
- icon: Expensicons.Task,
- text: translate('newTaskPage.assignTask'),
- onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()),
- },
- ],
+ ...(canUseTrackExpense
+ ? [
+ {
+ icon: Expensicons.DocumentPlus,
+ text: translate('iou.trackExpense'),
+ onSelected: () =>
+ interceptAnonymousUser(() =>
+ IOU.startMoneyRequest(
+ CONST.IOU.TYPE.TRACK_EXPENSE,
+ // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID.
+ // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow.
+ ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(),
+ ),
+ ),
+ },
+ ]
+ : []),
+ {
+ icon: Expensicons.Task,
+ text: translate('newTaskPage.assignTask'),
+ onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()),
+ },
{
icon: Expensicons.Heart,
text: translate('sidebarScreen.saveTheWorld'),
diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js
index 589808824285..cb078fac133c 100644
--- a/src/pages/iou/request/IOURequestStartPage.js
+++ b/src/pages/iou/request/IOURequestStartPage.js
@@ -78,6 +78,7 @@ function IOURequestStartPage({
[CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'),
[CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'),
[CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'),
+ [CONST.IOU.TYPE.TRACK_EXPENSE]: translate('iou.trackExpense'),
};
const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction));
const previousIOURequestType = usePrevious(transactionRequestType.current);
@@ -109,7 +110,7 @@ function IOURequestStartPage({
const isExpenseChat = ReportUtils.isPolicyExpenseChat(report);
const isExpenseReport = ReportUtils.isExpenseReport(report);
- const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate;
+ const shouldDisplayDistanceRequest = iouType !== CONST.IOU.TYPE.TRACK_EXPENSE && (canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate);
// Allow the user to create the request if we are creating the request in global menu or the report can create the request
const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType);
@@ -157,7 +158,7 @@ function IOURequestStartPage({
title={tabTitles[iouType]}
onBackButtonPress={navigateBack}
/>
- {iouType === CONST.IOU.TYPE.REQUEST || iouType === CONST.IOU.TYPE.SPLIT ? (
+ {iouType !== CONST.IOU.TYPE.SEND ? (
{
- onParticipantsAdded([
- {
- ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'),
- selected: true,
- },
- ]);
- onFinish();
- };
+ const addSingleParticipant = useCallback(
+ (option) => {
+ onParticipantsAdded([
+ {
+ ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'),
+ selected: true,
+ },
+ ]);
+ onFinish();
+ },
+ [onFinish, onParticipantsAdded],
+ );
/**
* Removes a selected option from list if already selected. If not already selected add this option to the list.
@@ -257,13 +260,22 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({
const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant;
const isAllowedToSplit = (canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE) && iouType !== CONST.IOU.TYPE.SEND;
- const handleConfirmSelection = useCallback(() => {
- if (shouldShowSplitBillErrorMessage) {
- return;
- }
+ const handleConfirmSelection = useCallback(
+ (keyEvent, option) => {
+ const shouldAddSingleParticipant = option && !participants.length;
+ if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) {
+ return;
+ }
- onFinish(CONST.IOU.TYPE.SPLIT);
- }, [shouldShowSplitBillErrorMessage, onFinish]);
+ if (shouldAddSingleParticipant) {
+ addSingleParticipant(option);
+ return;
+ }
+
+ onFinish(CONST.IOU.TYPE.SPLIT);
+ },
+ [shouldShowSplitBillErrorMessage, onFinish, addSingleParticipant, participants],
+ );
const footerContent = useMemo(
() => (
@@ -360,8 +372,7 @@ MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTempora
export default withOnyx({
dismissedReferralBanners: {
- key: ONYXKEYS.ACCOUNT,
- selector: (data) => data.dismissedReferralBanners || {},
+ key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
},
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js
index 2c869354d96f..3dd6f08c0ce0 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.js
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js
@@ -100,6 +100,9 @@ function IOURequestStepConfirmation({
if (iouType === CONST.IOU.TYPE.SPLIT) {
return translate('iou.split');
}
+ if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) {
+ return translate('iou.trackExpense');
+ }
if (iouType === CONST.IOU.TYPE.SEND) {
return translate('common.send');
}
@@ -109,8 +112,8 @@ function IOURequestStepConfirmation({
const participants = useMemo(
() =>
_.map(transaction.participants, (participant) => {
- const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false);
- return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails);
+ const participantReportID = lodashGet(participant, 'reportID', '');
+ return participantReportID ? OptionsListUtils.getReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails);
}),
[transaction.participants, personalDetails],
);
@@ -131,7 +134,7 @@ function IOURequestStepConfirmation({
if (policyExpenseChat) {
Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID);
}
- }, [participants, transaction.billable, policy, transactionID]);
+ }, [isOffline, participants, transaction.billable, policy, transactionID]);
const defaultBillable = lodashGet(policy, 'defaultBillable', false);
useEffect(() => {
@@ -187,13 +190,6 @@ function IOURequestStepConfirmation({
IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType);
}, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]);
- useEffect(() => {
- const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat);
- if (policyExpenseChat) {
- Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID);
- }
- }, [isOffline, participants, transaction.billable, policy]);
-
/**
* @param {Array} selectedParticipants
* @param {String} trimmedComment
@@ -226,6 +222,54 @@ function IOURequestStepConfirmation({
[report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories],
);
+ /**
+ * @param {Array} selectedParticipants
+ * @param {String} trimmedComment
+ * @param {File} [receiptObj]
+ */
+ const trackExpense = useCallback(
+ (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => {
+ IOU.trackExpense(
+ report,
+ transaction.amount,
+ transaction.currency,
+ transaction.created,
+ transaction.merchant,
+ currentUserPersonalDetails.login,
+ currentUserPersonalDetails.accountID,
+ selectedParticipants[0],
+ trimmedComment,
+ receiptObj,
+ transaction.category,
+ transaction.tag,
+ transactionTaxCode,
+ transactionTaxAmount,
+ transaction.billable,
+ policy,
+ policyTags,
+ policyCategories,
+ gpsPoints,
+ );
+ },
+ [
+ report,
+ transaction.amount,
+ transaction.currency,
+ transaction.created,
+ transaction.merchant,
+ transaction.category,
+ transaction.tag,
+ transaction.billable,
+ currentUserPersonalDetails.login,
+ currentUserPersonalDetails.accountID,
+ transactionTaxCode,
+ transactionTaxAmount,
+ policy,
+ policyTags,
+ policyCategories,
+ ],
+ );
+
/**
* @param {Array} selectedParticipants
* @param {String} trimmedComment
@@ -319,6 +363,41 @@ function IOURequestStepConfirmation({
return;
}
+ if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) {
+ if (receiptFile) {
+ // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included.
+ if (transaction.amount === 0) {
+ getCurrentPosition(
+ (successData) => {
+ trackExpense(selectedParticipants, trimmedComment, receiptFile, {
+ lat: successData.coords.latitude,
+ long: successData.coords.longitude,
+ });
+ },
+ (errorData) => {
+ Log.info('[IOURequestStepConfirmation] getCurrentPosition failed', false, errorData);
+ // When there is an error, the money can still be requested, it just won't include the GPS coordinates
+ trackExpense(selectedParticipants, trimmedComment, receiptFile);
+ },
+ {
+ // It's OK to get a cached location that is up to an hour old because the only accuracy needed is the country the user is in
+ maximumAge: 1000 * 60 * 60,
+
+ // 15 seconds, don't wait too long because the server can always fall back to using the IP address
+ timeout: 15000,
+ },
+ );
+ return;
+ }
+
+ // Otherwise, the money is being requested through the "Manual" flow with an attached image and the GPS coordinates are not needed.
+ trackExpense(selectedParticipants, trimmedComment, receiptFile);
+ return;
+ }
+ trackExpense(selectedParticipants, trimmedComment, receiptFile);
+ return;
+ }
+
if (receiptFile) {
// If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included.
if (transaction.amount === 0) {
@@ -357,7 +436,18 @@ function IOURequestStepConfirmation({
requestMoney(selectedParticipants, trimmedComment);
},
- [iouType, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, report, requestType, createDistanceRequest, requestMoney, receiptFile],
+ [
+ transaction,
+ iouType,
+ receiptFile,
+ requestType,
+ requestMoney,
+ currentUserPersonalDetails.login,
+ currentUserPersonalDetails.accountID,
+ report.reportID,
+ trackExpense,
+ createDistanceRequest,
+ ],
);
/**
@@ -417,7 +507,7 @@ function IOURequestStepConfirmation({
{
- if (participants.length) {
- return;
- }
- onAddParticipants(
- [
- {
- accountID: option.accountID,
- login: option.login,
- isPolicyExpenseChat: option.isPolicyExpenseChat,
- reportID: option.reportID,
- selected: true,
- searchText: option.searchText,
- },
- ],
- false,
- );
- navigateToRequest();
- };
+ const addSingleParticipant = useCallback(
+ (option) => {
+ if (participants.length) {
+ return;
+ }
+ onAddParticipants(
+ [
+ {
+ accountID: option.accountID,
+ login: option.login,
+ isPolicyExpenseChat: option.isPolicyExpenseChat,
+ reportID: option.reportID,
+ selected: true,
+ searchText: option.searchText,
+ },
+ ],
+ false,
+ );
+ navigateToRequest();
+ },
+ [navigateToRequest, onAddParticipants, participants.length],
+ );
/**
* Removes a selected option from list if already selected. If not already selected add this option to the list.
@@ -275,13 +278,23 @@ function MoneyRequestParticipantsSelector({
const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant;
const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && iouType !== CONST.IOU.TYPE.SEND;
- const handleConfirmSelection = useCallback(() => {
- if (shouldShowSplitBillErrorMessage) {
- return;
- }
+ const handleConfirmSelection = useCallback(
+ (keyEvent, option) => {
+ const shouldAddSingleParticipant = option && !participants.length;
- navigateToSplit();
- }, [shouldShowSplitBillErrorMessage, navigateToSplit]);
+ if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) {
+ return;
+ }
+
+ if (shouldAddSingleParticipant) {
+ addSingleParticipant(option);
+ return;
+ }
+
+ navigateToSplit();
+ },
+ [shouldShowSplitBillErrorMessage, navigateToSplit, addSingleParticipant, participants.length],
+ );
const footerContent = useMemo(
() => (
@@ -373,8 +386,7 @@ MoneyRequestParticipantsSelector.defaultProps = defaultProps;
export default withOnyx({
dismissedReferralBanners: {
- key: ONYXKEYS.ACCOUNT,
- selector: (data) => data.dismissedReferralBanners || {},
+ key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS,
},
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
diff --git a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx
new file mode 100644
index 000000000000..3bcdc1fe3303
--- /dev/null
+++ b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx
@@ -0,0 +1,74 @@
+/* eslint-disable rulesdir/no-negated-variables */
+import React, {useEffect} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as Policy from '@userActions/Policy';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {PolicyFeatureName} from '@src/types/onyx/Policy';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+
+type FeatureEnabledAccessOrNotFoundOnyxProps = {
+ /** The report currently being looked at */
+ policy: OnyxEntry;
+
+ /** Indicated whether the report data is loading */
+ isLoadingReportData: OnyxEntry;
+};
+
+type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFoundOnyxProps & {
+ /** The children to render */
+ children: ((props: FeatureEnabledAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode;
+
+ /** The report currently being looked at */
+ policyID: string;
+
+ /** The current feature name that the user tries to get access */
+ featureName: PolicyFeatureName;
+};
+
+function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNotFoundComponentProps) {
+ const isPolicyIDInRoute = !!props.policyID?.length;
+ const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id);
+ const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName);
+
+ useEffect(() => {
+ if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) {
+ // If the workspace is not required or is already loaded, we don't need to call the API
+ return;
+ }
+
+ Policy.openWorkspace(props.policyID, []);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [isPolicyIDInRoute, props.policyID]);
+
+ if (shouldShowFullScreenLoadingIndicator) {
+ return ;
+ }
+
+ if (shouldShowNotFoundPage) {
+ return (
+ Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
+ shouldForceFullScreen
+ />
+ );
+ }
+
+ return typeof props.children === 'function' ? props.children(props) : props.children;
+}
+
+export default withOnyx({
+ policy: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`,
+ },
+ isLoadingReportData: {
+ key: ONYXKEYS.IS_LOADING_REPORT_DATA,
+ },
+})(FeatureEnabledAccessOrNotFoundComponent);
diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx
index 69f2d74b6be7..d92c650fa9c7 100644
--- a/src/pages/workspace/WorkspaceNewRoomPage.tsx
+++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx
@@ -35,7 +35,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {NewRoomForm} from '@src/types/form/NewRoomForm';
import INPUT_IDS from '@src/types/form/NewRoomForm';
-import type {Account, Policy, Report as ReportType, Session} from '@src/types/onyx';
+import type {Policy, Report as ReportType, Session} from '@src/types/onyx';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -53,7 +53,7 @@ type WorkspaceNewRoomPageOnyxProps = {
session: OnyxEntry;
/** policyID for main workspace */
- activePolicyID: OnyxEntry['activePolicyID']>;
+ activePolicyID: OnyxEntry>;
};
type WorkspaceNewRoomPageProps = WorkspaceNewRoomPageOnyxProps;
@@ -343,8 +343,7 @@ export default withOnyx account?.activePolicyID ?? null,
+ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
initialValue: null,
},
})(WorkspaceNewRoomPage);
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index d110a5752382..7169d8a4ab7c 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -20,6 +20,7 @@ import useLocalize from '@hooks/useLocalize';
import useThemeIllustrations from '@hooks/useThemeIllustrations';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
@@ -173,7 +174,11 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi
/>
{(!StringUtils.isEmptyString(policy?.description ?? '') || !readOnly) && (
-
+ Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)}
+ >
)}
-
+ Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)}
+ errorRowStyles={[styles.mt2]}
+ >
(null);
const {isSmallScreenWidth} = useWindowDimensions();
+ const session = useSession();
const policyName = policy?.name ?? '';
const id = policy?.id ?? '';
+ const adminEmail = session?.email ?? '';
const urlWithTrailingSlash = Url.addTrailingForwardSlash(environmentURL);
- const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_PROFILE.getRoute(id)}`;
+ const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_JOIN_USER.getRoute(id, adminEmail)}`;
+
return (
-
-
- setDeleteCategoryConfirmModalVisible(false)}
- title={translate('workspace.categories.deleteCategory')}
- prompt={translate('workspace.categories.deleteCategoryPrompt')}
- confirmText={translate('common.delete')}
- cancelText={translate('common.cancel')}
- danger
- />
-
- Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)}
- >
-
-
- {translate('workspace.categories.enableCategory')}
-
-
-
-
-
+
+ setDeleteCategoryConfirmModalVisible(false)}
+ title={translate('workspace.categories.deleteCategory')}
+ prompt={translate('workspace.categories.deleteCategoryPrompt')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
/>
-
-
+
+ Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)}
+ >
+
+
+ {translate('workspace.categories.enableCategory')}
+
+
+
+
+
+
+
+
);
diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx
index 80370d2197fa..b31207e73208 100644
--- a/src/pages/workspace/categories/CreateCategoryPage.tsx
+++ b/src/pages/workspace/categories/CreateCategoryPage.tsx
@@ -10,8 +10,10 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type {PolicyCategories} from '@src/types/onyx';
@@ -38,21 +40,26 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps)
return (
-
-
-
-
+
+
+
+
+
);
diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx
index 11df2f195f9d..0e5ed0589934 100644
--- a/src/pages/workspace/categories/EditCategoryPage.tsx
+++ b/src/pages/workspace/categories/EditCategoryPage.tsx
@@ -10,8 +10,10 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -40,22 +42,27 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) {
return (
-
- Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))}
- />
-
-
+
+ Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))}
+ />
+
+
+
);
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
index 3f929e46ab67..f3456c3875f5 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
@@ -28,6 +28,7 @@ import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
@@ -263,62 +264,67 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat
return (
-
-
- {!isSmallScreenWidth && getHeaderButtons()}
-
- setDeleteCategoriesConfirmModalVisible(false)}
- title={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategory' : 'workspace.categories.deleteCategories')}
- prompt={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategoryPrompt' : 'workspace.categories.deleteCategoriesPrompt')}
- confirmText={translate('common.delete')}
- cancelText={translate('common.cancel')}
- danger
- />
- {isSmallScreenWidth && {getHeaderButtons()}}
-
- {translate('workspace.categories.subtitle')}
-
- {isLoading && (
-
- )}
- {shouldShowEmptyState && (
-
- )}
- {!shouldShowEmptyState && (
-
+ {!isSmallScreenWidth && getHeaderButtons()}
+
+ setDeleteCategoriesConfirmModalVisible(false)}
+ title={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategory' : 'workspace.categories.deleteCategories')}
+ prompt={translate(selectedCategoriesArray.length === 1 ? 'workspace.categories.deleteCategoryPrompt' : 'workspace.categories.deleteCategoriesPrompt')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
/>
- )}
-
+ {isSmallScreenWidth && {getHeaderButtons()}}
+
+ {translate('workspace.categories.subtitle')}
+
+ {isLoading && (
+
+ )}
+ {shouldShowEmptyState && (
+
+ )}
+ {!shouldShowEmptyState && (
+
+ )}
+
+
);
diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx
index 6939bac56894..0ec937b19ba2 100644
--- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx
+++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx
@@ -11,7 +11,9 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {setWorkspaceRequiresCategory} from '@libs/actions/Policy';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import CONST from '@src/CONST';
import type SCREENS from '@src/SCREENS';
type WorkspaceCategoriesSettingsPageProps = StackScreenProps;
@@ -27,34 +29,39 @@ function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPag
return (
- {({policy}) => (
-
-
-
-
-
-
- {translate('workspace.categories.requiresCategory')}
-
+
+ {({policy}) => (
+
+
+
+
+
+
+ {translate('workspace.categories.requiresCategory')}
+
+
-
-
-
-
- )}
+
+
+
+ )}
+
);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
index 0b2ef794a4ca..32531da0dd25 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx
@@ -24,6 +24,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu';
@@ -237,53 +238,58 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps)
return (
-
-
- {!isSmallScreenWidth && headerButtons}
-
- {isSmallScreenWidth && {headerButtons}}
-
- {translate('workspace.distanceRates.centrallyManage')}
-
- {isLoading && (
-
- )}
- {Object.values(customUnitRates).length > 0 && (
-
+ {!isSmallScreenWidth && headerButtons}
+
+ {isSmallScreenWidth && {headerButtons}}
+
+ {translate('workspace.distanceRates.centrallyManage')}
+
+ {isLoading && (
+
+ )}
+ {Object.values(customUnitRates).length > 0 && (
+
+ )}
+ setIsWarningModalVisible(false)}
+ isVisible={isWarningModalVisible}
+ title={translate('workspace.distanceRates.oopsNotSoFast')}
+ prompt={translate('workspace.distanceRates.workspaceNeeds')}
+ confirmText={translate('common.buttonConfirm')}
+ shouldShowCancelButton={false}
/>
- )}
- setIsWarningModalVisible(false)}
- isVisible={isWarningModalVisible}
- title={translate('workspace.distanceRates.oopsNotSoFast')}
- prompt={translate('workspace.distanceRates.workspaceNeeds')}
- confirmText={translate('common.buttonConfirm')}
- shouldShowCancelButton={false}
- />
-
+
+
);
diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
index f650a618250e..83b096db1301 100644
--- a/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
+++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage.tsx
@@ -11,8 +11,10 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
@@ -55,42 +57,47 @@ function PolicyDistanceRatesSettingsPage({policy, route}: PolicyDistanceRatesSet
return (
-
-
- clearErrorFields('attributes')}
+
-
-
- {policy?.areCategoriesEnabled && (
+
clearErrorFields('defaultCategory')}
+ onClose={() => clearErrorFields('attributes')}
>
-
- )}
-
+ {policy?.areCategoriesEnabled && (
+ clearErrorFields('defaultCategory')}
+ >
+
+
+ )}
+
+
);
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
index d83f1b1d77a7..709e51cba383 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage.tsx
@@ -44,7 +44,7 @@ function WorkspaceRatePage(props: WorkspaceRatePageProps) {
const submit = (values: FormOnyxValues) => {
const rate = values.rate;
Policy.setRateForReimburseView((parseFloat(rate) * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET).toFixed(1));
- Navigation.navigate(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? ''));
+ Navigation.goBack(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? ''));
};
const validate = useCallback(
diff --git a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx
index 36efc239fe69..1d30c068e30d 100644
--- a/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx
+++ b/src/pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage.tsx
@@ -38,7 +38,7 @@ function WorkspaceUnitPage(props: WorkspaceUnitPageProps) {
const updateUnit = (unit: UnitItemType) => {
Policy.setUnitForReimburseView(unit.value);
- Navigation.navigate(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? ''));
+ Navigation.goBack(ROUTES.WORKSPACE_RATE_AND_UNIT.getRoute(props.policy?.id ?? ''));
};
const defaultValue = useMemo(() => {
diff --git a/src/pages/workspace/tags/EditTagPage.tsx b/src/pages/workspace/tags/EditTagPage.tsx
index c22ba9154146..92d7c0a11ac9 100644
--- a/src/pages/workspace/tags/EditTagPage.tsx
+++ b/src/pages/workspace/tags/EditTagPage.tsx
@@ -17,6 +17,7 @@ import * as PolicyUtils from '@libs/PolicyUtils';
import * as ValidationUtils from '@libs/ValidationUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
@@ -66,36 +67,41 @@ function EditTagPage({route, policyTags}: EditTagPageProps) {
return (
-
-
-
-
-
-
+
+
+
+
+
);
diff --git a/src/pages/workspace/tags/TagSettingsPage.tsx b/src/pages/workspace/tags/TagSettingsPage.tsx
index 5f164a25e5fe..107689bc46b9 100644
--- a/src/pages/workspace/tags/TagSettingsPage.tsx
+++ b/src/pages/workspace/tags/TagSettingsPage.tsx
@@ -21,8 +21,10 @@ import * as PolicyUtils from '@libs/PolicyUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -67,61 +69,66 @@ function TagSettingsPage({route, policyTags}: TagSettingsPageProps) {
return (
-
- setIsDeleteTagModalOpen(true),
- },
- ]}
- />
- setIsDeleteTagModalOpen(false)}
- shouldSetModalVisibility={false}
- prompt={translate('workspace.tags.deleteTagConfirmation')}
- confirmText={translate('common.delete')}
- cancelText={translate('common.cancel')}
- danger
- />
-
- Policy.clearPolicyTagErrors(route.params.policyID, route.params.tagName)}
- >
-
-
- {translate('workspace.tags.enableTag')}
-
-
-
-
-
+ setIsDeleteTagModalOpen(true),
+ },
+ ]}
+ />
+ setIsDeleteTagModalOpen(false)}
+ shouldSetModalVisibility={false}
+ prompt={translate('workspace.tags.deleteTagConfirmation')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
/>
-
-
+
+ Policy.clearPolicyTagErrors(route.params.policyID, route.params.tagName)}
+ >
+
+
+ {translate('workspace.tags.enableTag')}
+
+
+
+
+
+
+
+
);
diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
index 04c0cf8038d0..346d56891dd5 100644
--- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx
@@ -18,6 +18,7 @@ import * as PolicyUtils from '@libs/PolicyUtils';
import * as ValidationUtils from '@libs/ValidationUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
@@ -70,35 +71,40 @@ function CreateTagPage({route, policyTags}: CreateTagPageProps) {
return (
-
-
-
-
-
-
+
+
+
+
+
);
diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx
index 98ae6f726d73..0072d37ef631 100644
--- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx
@@ -16,6 +16,9 @@ import * as Policy from '@libs/actions/Policy';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
@@ -52,33 +55,42 @@ function WorkspaceEditTagsPage({route, policyTags}: WorkspaceEditTagsPageProps)
);
return (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
index 126d548c2c8a..a355cc062f3d 100644
--- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx
@@ -27,6 +27,7 @@ import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
@@ -254,62 +255,67 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) {
return (
-
-
- {!isSmallScreenWidth && getHeaderButtons()}
-
- setDeleteTagsConfirmModalVisible(false)}
- title={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags')}
- prompt={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTagConfirmation' : 'workspace.tags.deleteTagsConfirmation')}
- confirmText={translate('common.delete')}
- cancelText={translate('common.cancel')}
- danger
- />
- {isSmallScreenWidth && {getHeaderButtons()}}
-
- {translate('workspace.tags.subtitle')}
-
- {isLoading && (
-
- )}
- {tagList.length === 0 && !isLoading && (
-
- )}
- {tagList.length > 0 && (
- Policy.clearPolicyTagErrors(route.params.policyID, item.value)}
+
+ {!isSmallScreenWidth && getHeaderButtons()}
+
+ setDeleteTagsConfirmModalVisible(false)}
+ title={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTag' : 'workspace.tags.deleteTags')}
+ prompt={translate(selectedTagsArray.length === 1 ? 'workspace.tags.deleteTagConfirmation' : 'workspace.tags.deleteTagsConfirmation')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
/>
- )}
-
+ {isSmallScreenWidth && {getHeaderButtons()}}
+
+ {translate('workspace.tags.subtitle')}
+
+ {isLoading && (
+
+ )}
+ {tagList.length === 0 && !isLoading && (
+
+ )}
+ {tagList.length > 0 && (
+ Policy.clearPolicyTagErrors(route.params.policyID, item.value)}
+ />
+ )}
+
+
);
diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx
index 67b033b68f72..b421698b8f2f 100644
--- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx
+++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx
@@ -16,7 +16,9 @@ import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -39,49 +41,53 @@ function WorkspaceTagsSettingsPage({route, policyTags}: WorkspaceTagsSettingsPag
},
[route.params.policyID],
);
-
return (
- {({policy}) => (
-
-
-
-
-
-
- {translate('workspace.tags.requiresTag')}
-
+
+ {({policy}) => (
+
+
+
+
+
+
+ {translate('workspace.tags.requiresTag')}
+
+
-
-
-
- Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))}
- shouldShowRightIcon
- />
-
-
-
- )}
+
+
+ Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))}
+ shouldShowRightIcon
+ />
+
+
+
+ )}
+
);
diff --git a/src/pages/workspace/taxes/NamePage.tsx b/src/pages/workspace/taxes/NamePage.tsx
new file mode 100644
index 000000000000..1efb983be19e
--- /dev/null
+++ b/src/pages/workspace/taxes/NamePage.tsx
@@ -0,0 +1,120 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import ExpensiMark from 'expensify-common/lib/ExpensiMark';
+import React, {useCallback, useState} from 'react';
+import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {renamePolicyTax, validateTaxName} from '@libs/actions/TaxRate';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/WorkspaceTaxNameForm';
+
+type NamePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+
+const parser = new ExpensiMark();
+
+function NamePage({
+ route: {
+ params: {policyID, taxID},
+ },
+ policy,
+}: NamePageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID);
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const [name, setName] = useState(() => parser.htmlToMarkdown(currentTaxRate?.name ?? ''));
+
+ const goBack = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_TAX_EDIT.getRoute(policyID ?? '', taxID)), [policyID, taxID]);
+
+ const submit = () => {
+ renamePolicyTax(policyID, taxID, name);
+ goBack();
+ };
+
+ const validate = useCallback(
+ (values: FormOnyxValues) => {
+ if (!policy) {
+ return {};
+ }
+ if (values[INPUT_IDS.NAME] === currentTaxRate?.name) {
+ return {};
+ }
+ return validateTaxName(policy, values);
+ },
+ [currentTaxRate?.name, policy],
+ );
+
+ if (!currentTaxRate) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+NamePage.displayName = 'NamePage';
+
+export default withPolicyAndFullscreenLoading(NamePage);
diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx
new file mode 100644
index 000000000000..d008b11ecb15
--- /dev/null
+++ b/src/pages/workspace/taxes/ValuePage.tsx
@@ -0,0 +1,103 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useState} from 'react';
+import AmountForm from '@components/AmountForm';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {updatePolicyTaxValue, validateTaxValue} from '@libs/actions/TaxRate';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/WorkspaceTaxValueForm';
+
+type ValuePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+
+function ValuePage({
+ route: {
+ params: {policyID, taxID},
+ },
+ policy,
+}: ValuePageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID);
+ const [value, setValue] = useState(currentTaxRate?.value?.replace('%', ''));
+
+ const goBack = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_TAX_EDIT.getRoute(policyID ?? '', taxID)), [policyID, taxID]);
+
+ const submit = useCallback(
+ (values: FormOnyxValues) => {
+ updatePolicyTaxValue(policyID, taxID, Number(values.value));
+ goBack();
+ },
+ [goBack, policyID, taxID],
+ );
+
+ if (!currentTaxRate) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+ %}
+ />
+
+
+
+
+
+ );
+}
+
+ValuePage.displayName = 'ValuePage';
+
+export default withPolicyAndFullscreenLoading(ValuePage);
diff --git a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
index c0790bb8abd8..ccc0d4ad9e7b 100644
--- a/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceCreateTaxPage.tsx
@@ -11,11 +11,11 @@ import Text from '@components/Text';
import TextPicker from '@components/TextPicker';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import {createPolicyTax, getNextTaxCode, getTaxValueWithPercentage} from '@libs/actions/TaxRate';
+import {createPolicyTax, getNextTaxCode, getTaxValueWithPercentage, validateTaxName, validateTaxValue} from '@libs/actions/TaxRate';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
-import * as ValidationUtils from '@libs/ValidationUtils';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -36,25 +36,6 @@ function WorkspaceCreateTaxPage({
const styles = useThemeStyles();
const {translate} = useLocalize();
- const validate = useCallback(
- (values: FormOnyxValues): FormInputErrors => {
- const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.VALUE, INPUT_IDS.NAME]);
-
- const value = values[INPUT_IDS.VALUE];
- if (!ValidationUtils.isValidPercentage(value)) {
- errors[INPUT_IDS.VALUE] = 'workspace.taxes.errors.valuePercentageRange';
- }
-
- const name = values[INPUT_IDS.NAME];
- if (policy?.taxRates?.taxes && ValidationUtils.isExistingTaxName(name, policy.taxRates.taxes)) {
- errors[INPUT_IDS.NAME] = 'workspace.taxes.errors.taxRateAlreadyExists';
- }
-
- return errors;
- },
- [policy?.taxRates?.taxes],
- );
-
const submitForm = useCallback(
({value, ...values}: FormOnyxValues) => {
const taxRate = {
@@ -68,52 +49,70 @@ function WorkspaceCreateTaxPage({
[policy?.taxRates?.taxes, policyID],
);
+ const validateForm = useCallback(
+ (values: FormOnyxValues): FormInputErrors => {
+ if (!policy) {
+ return {};
+ }
+ return {
+ ...validateTaxName(policy, values),
+ ...validateTaxValue(values),
+ };
+ },
+ [policy],
+ );
+
return (
-
-
-
-
-
-
- (v ? getTaxValueWithPercentage(v) : '')}
- description={translate('workspace.taxes.value')}
- rightLabel={translate('common.required')}
- hideCurrencySymbol
- extraSymbol={%}
- />
-
-
-
-
+
+
+
+
+
+
+ (v ? getTaxValueWithPercentage(v) : '')}
+ description={translate('workspace.taxes.value')}
+ rightLabel={translate('common.required')}
+ hideCurrencySymbol
+ extraSymbol={%}
+ />
+
+
+
+
+
);
diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx
new file mode 100644
index 000000000000..ec04b77df3ca
--- /dev/null
+++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx
@@ -0,0 +1,163 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useMemo, useState} from 'react';
+import {View} from 'react-native';
+import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import type {ThreeDotsMenuItem} from '@components/HeaderWithBackButton/types';
+import * as Expensicons from '@components/Icon/Expensicons';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Switch from '@components/Switch';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import {clearTaxRateFieldError, deletePolicyTaxes, setPolicyTaxesEnabled} from '@libs/actions/TaxRate';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+
+type WorkspaceEditTaxPageBaseProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
+
+function WorkspaceEditTaxPage({
+ route: {
+ params: {policyID, taxID},
+ },
+ policy,
+}: WorkspaceEditTaxPageBaseProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID);
+ const {windowWidth} = useWindowDimensions();
+ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
+ const canEdit = policy && PolicyUtils.canEditTaxRate(policy, taxID);
+
+ const toggleTaxRate = () => {
+ if (!currentTaxRate) {
+ return;
+ }
+ setPolicyTaxesEnabled(policyID, [taxID], !!currentTaxRate.isDisabled);
+ };
+
+ const deleteTaxRate = () => {
+ if (!policyID) {
+ return;
+ }
+ deletePolicyTaxes(policyID, [taxID]);
+ setIsDeleteModalVisible(false);
+ Navigation.goBack();
+ };
+
+ const threeDotsMenuItems: ThreeDotsMenuItem[] = useMemo(
+ () => [
+ {
+ icon: Expensicons.Trashcan,
+ text: translate('common.delete'),
+ onSelected: () => setIsDeleteModalVisible(true),
+ },
+ ],
+ [translate],
+ );
+
+ if (!currentTaxRate) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+ clearTaxRateFieldError(policyID, taxID, 'isDisabled')}
+ >
+
+
+ {translate('workspace.taxes.actions.enable')}
+
+
+
+
+ clearTaxRateFieldError(policyID, taxID, 'name')}
+ >
+ Navigation.navigate(ROUTES.WORKSPACE_TAX_NAME.getRoute(`${policyID}`, taxID))}
+ />
+
+ clearTaxRateFieldError(policyID, taxID, 'value')}
+ >
+ Navigation.navigate(ROUTES.WORKSPACE_TAX_VALUE.getRoute(`${policyID}`, taxID))}
+ />
+
+
+ setIsDeleteModalVisible(false)}
+ prompt={translate('workspace.taxes.deleteTaxConfirmation')}
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
+
+
+
+
+ );
+}
+
+WorkspaceEditTaxPage.displayName = 'WorkspaceEditTaxPage';
+
+export default withPolicyAndFullscreenLoading(WorkspaceEditTaxPage);
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
index 8eb730c0134f..bad82d827c5d 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx
@@ -1,7 +1,10 @@
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {ActivityIndicator, View} from 'react-native';
import Button from '@components/Button';
+import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
+import type {DropdownOption, WorkspaceTaxRatesBulkActionType} from '@components/ButtonWithDropdownMenu/types';
+import ConfirmModal from '@components/ConfirmModal';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -17,10 +20,13 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {openPolicyTaxesPage} from '@libs/actions/Policy';
-import {clearTaxRateError} from '@libs/actions/TaxRate';
+import {clearTaxRateError, deletePolicyTaxes, setPolicyTaxesEnabled} from '@libs/actions/TaxRate';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
import type {WorkspacesCentralPaneNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -30,17 +36,24 @@ import type SCREENS from '@src/SCREENS';
type WorkspaceTaxesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps;
-function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) {
+function WorkspaceTaxesPage({
+ policy,
+ route: {
+ params: {policyID},
+ },
+}: WorkspaceTaxesPageProps) {
const {isSmallScreenWidth} = useWindowDimensions();
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
const [selectedTaxesIDs, setSelectedTaxesIDs] = useState([]);
+ const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const defaultExternalID = policy?.taxRates?.defaultExternalID;
const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault;
+ const dropdownButtonRef = useRef(null);
const fetchTaxes = () => {
- openPolicyTaxesPage(route.params.policyID);
+ openPolicyTaxesPage(policyID);
};
const {isOffline} = useNetwork({onReconnect: fetchTaxes});
@@ -66,34 +79,34 @@ function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) {
[defaultExternalID, foreignTaxDefault, translate],
);
- const taxesList = useMemo(
- () =>
- Object.entries(policy?.taxRates?.taxes ?? {})
- .map(([key, value]) => ({
- text: value.name,
- alternateText: textForDefault(key),
- keyForList: key,
- isSelected: !!selectedTaxesIDs.includes(key),
- isDisabledCheckbox: key === defaultExternalID,
- pendingAction: value.pendingAction,
- errors: value.errors,
- rightElement: (
-
-
- {value.isDisabled ? translate('workspace.common.disabled') : translate('workspace.common.enabled')}
-
-
-
-
+ const taxesList = useMemo(() => {
+ if (!policy) {
+ return [];
+ }
+ return Object.entries(policy.taxRates?.taxes ?? {})
+ .map(([key, value]) => ({
+ text: value.name,
+ alternateText: textForDefault(key),
+ keyForList: key,
+ isSelected: !!selectedTaxesIDs.includes(key),
+ isDisabledCheckbox: !PolicyUtils.canEditTaxRate(policy, key),
+ isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
+ pendingAction: value.pendingAction ?? (Object.keys(value.pendingFields ?? {}).length > 0 ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null),
+ errors: value.errors ?? ErrorUtils.getLatestErrorFieldForAnyField(value),
+ rightElement: (
+
+ {value.isDisabled ? translate('workspace.common.disabled') : translate('workspace.common.enabled')}
+
+
- ),
- }))
- .sort((a, b) => a.text.localeCompare(b.text)),
- [policy?.taxRates?.taxes, textForDefault, defaultExternalID, selectedTaxesIDs, styles, theme.icon, translate],
- );
+
+ ),
+ }))
+ .sort((a, b) => (a.text ?? a.keyForList ?? '').localeCompare(b.text ?? b.keyForList ?? ''));
+ }, [policy, textForDefault, selectedTaxesIDs, styles.flexRow, styles.disabledText, styles.alignSelfCenter, styles.p1, styles.pl2, translate, theme.icon]);
const isLoading = !isOffline && taxesList === undefined;
@@ -129,68 +142,159 @@ function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) {
);
- const headerButtons = (
+ const deleteTaxes = useCallback(() => {
+ if (!policyID) {
+ return;
+ }
+ deletePolicyTaxes(policyID, selectedTaxesIDs);
+ setSelectedTaxesIDs([]);
+ setIsDeleteModalVisible(false);
+ }, [policyID, selectedTaxesIDs]);
+
+ const toggleTaxes = useCallback(
+ (isEnabled: boolean) => {
+ if (!policyID) {
+ return;
+ }
+ setPolicyTaxesEnabled(policyID, selectedTaxesIDs, isEnabled);
+ setSelectedTaxesIDs([]);
+ },
+ [policyID, selectedTaxesIDs],
+ );
+
+ const navigateToEditTaxRate = (taxRate: ListItem) => {
+ if (!taxRate.keyForList) {
+ return;
+ }
+ setSelectedTaxesIDs([]);
+ Navigation.navigate(ROUTES.WORKSPACE_TAX_EDIT.getRoute(policyID, taxRate.keyForList));
+ };
+
+ const dropdownMenuOptions = useMemo(() => {
+ const isMultiple = selectedTaxesIDs.length > 1;
+ const options: Array> = [
+ {
+ icon: Expensicons.Trashcan,
+ text: isMultiple ? translate('workspace.taxes.actions.deleteMultiple') : translate('workspace.taxes.actions.delete'),
+ value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.DELETE,
+ onSelected: () => setIsDeleteModalVisible(true),
+ },
+ ];
+
+ // `Disable rates` when at least one enabled rate is selected.
+ if (selectedTaxesIDs.some((taxID) => !policy?.taxRates?.taxes[taxID]?.isDisabled)) {
+ options.push({
+ icon: Expensicons.DocumentSlash,
+ text: isMultiple ? translate('workspace.taxes.actions.disableMultiple') : translate('workspace.taxes.actions.disable'),
+ value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.DISABLE,
+ onSelected: () => toggleTaxes(false),
+ });
+ }
+
+ // `Enable rates` when at least one disabled rate is selected.
+ if (selectedTaxesIDs.some((taxID) => policy?.taxRates?.taxes[taxID]?.isDisabled)) {
+ options.push({
+ icon: Expensicons.Document,
+ text: isMultiple ? translate('workspace.taxes.actions.enableMultiple') : translate('workspace.taxes.actions.enable'),
+ value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.ENABLE,
+ onSelected: () => toggleTaxes(true),
+ });
+ }
+ return options;
+ }, [policy?.taxRates?.taxes, selectedTaxesIDs, toggleTaxes, translate]);
+
+ const headerButtons = !selectedTaxesIDs.length ? (
+ ) : (
+
+ buttonRef={dropdownButtonRef}
+ onPress={() => {}}
+ options={dropdownMenuOptions}
+ buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
+ customText={translate('workspace.common.selected', {selectedNumber: selectedTaxesIDs.length})}
+ shouldAlwaysShowDropdownMenu
+ pressOnEnter
+ style={[isSmallScreenWidth && styles.w50, isSmallScreenWidth && styles.mb3]}
+ />
);
return (
-
-
-
+
+
-
- {!isSmallScreenWidth && headerButtons}
-
+
+ {!isSmallScreenWidth && headerButtons}
+
- {isSmallScreenWidth && {headerButtons}}
+ {isSmallScreenWidth && {headerButtons}}
-
- {translate('workspace.taxes.subtitle')}
-
- {isLoading && (
-
+ {translate('workspace.taxes.subtitle')}
+
+ {isLoading && (
+
+ )}
+ (item.keyForList ? clearTaxRateError(policyID, item.keyForList, item.pendingAction) : undefined)}
+ />
+ setIsDeleteModalVisible(false)}
+ prompt={
+ selectedTaxesIDs.length > 1
+ ? translate('workspace.taxes.deleteMultipleTaxConfirmation', {taxAmount: selectedTaxesIDs.length})
+ : translate('workspace.taxes.deleteTaxConfirmation')
+ }
+ confirmText={translate('common.delete')}
+ cancelText={translate('common.cancel')}
+ danger
/>
- )}
- {}}
- onSelectAll={toggleAllTaxes}
- showScrollIndicator
- ListItem={TableListItem}
- customListHeader={getCustomListHeader()}
- listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
- onDismissError={(item) => (item.keyForList ? clearTaxRateError(route.params.policyID, item.keyForList, item.pendingAction) : undefined)}
- />
-
+
+
);
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx
index 892434ce2d52..5b08a06247b0 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx
@@ -13,6 +13,7 @@ import {setPolicyCustomTaxName} from '@libs/actions/Policy';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
@@ -43,37 +44,42 @@ function WorkspaceTaxesSettingsCustomTaxName({
return (
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx
index 4a6626a78286..91d543b51b09 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsForeignCurrency.tsx
@@ -11,9 +11,11 @@ import {setForeignCurrencyDefault} from '@libs/actions/Policy';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -37,27 +39,32 @@ function WorkspaceTaxesSettingsForeignCurrency({
return (
-
- {({insets}) => (
- <>
-
+
+ {({insets}) => (
+ <>
+
-
-
-
- >
- )}
-
+
+
+
+ >
+ )}
+
+
);
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx
index 1fe6abb96b4c..0d1a8f1629c7 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsPage.tsx
@@ -11,9 +11,11 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
+import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -55,31 +57,36 @@ function WorkspaceTaxesSettingsPage({
return (
-
-
-
-
- {menuItems.map((item) => (
-
-
-
- ))}
-
-
-
+
+
+
+
+ {menuItems.map((item) => (
+
+
+
+ ))}
+
+
+
+
);
diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx
index 68c50f3af830..2fe2985daa22 100644
--- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx
+++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsWorkspaceCurrency.tsx
@@ -11,9 +11,11 @@ import {setWorkspaceCurrencyDefault} from '@libs/actions/Policy';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
+import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
@@ -37,27 +39,32 @@ function WorkspaceTaxesSettingsWorkspaceCurrency({
return (
-
- {({insets}) => (
- <>
-
+
+ {({insets}) => (
+ <>
+
-
-
-
- >
- )}
-
+
+
+
+ >
+ )}
+
+
);
diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx
index cf66af726a72..1dd65ffb2390 100644
--- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx
+++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx
@@ -1,3 +1,4 @@
+import type {StackScreenProps} from '@react-navigation/stack';
import React, {useState} from 'react';
import {FlatList} from 'react-native-gesture-handler';
import type {ValueOf} from 'type-fest';
@@ -9,20 +10,24 @@ import ScreenWrapper from '@components/ScreenWrapper';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as ErrorUtils from '@libs/ErrorUtils';
import * as Localize from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
+import type {WorkspacesCentralPaneNavigatorParamList} from '@libs/Navigation/types';
import * as PolicyUtils from '@libs/PolicyUtils';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import withPolicy from '@pages/workspace/withPolicy';
import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
type AutoReportingFrequencyKey = Exclude, 'instant'>;
type Locale = ValueOf;
-type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps;
+type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps & StackScreenProps;
type WorkspaceAutoReportingFrequencyPageItem = {
text: string;
@@ -41,7 +46,7 @@ const getAutoReportingFrequencyDisplayNames = (locale: Locale): AutoReportingFre
[CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: Localize.translate(locale, 'workflowsPage.frequencies.manually'),
});
-function WorkspaceAutoReportingFrequencyPage({policy}: WorkspaceAutoReportingFrequencyPageProps) {
+function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoReportingFrequencyPageProps) {
const {translate, preferredLocale, toLocaleOrdinal} = useLocalize();
const styles = useThemeStyles();
const [isMonthlyFrequency, setIsMonthlyFrequency] = useState(policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY);
@@ -80,7 +85,12 @@ function WorkspaceAutoReportingFrequencyPage({policy}: WorkspaceAutoReportingFre
};
const monthlyFrequencyDetails = () => (
-
+ Policy.clearPolicyErrorField(policy?.id ?? '', CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING_OFFSET)}
+ errorRowStyles={[styles.ml7]}
+ >