From 99d0dd814ac20c9c6e90e158fd0b9f3df6153b07 Mon Sep 17 00:00:00 2001 From: Stevie LaFortune Date: Wed, 2 Oct 2024 16:09:35 -0500 Subject: [PATCH 001/533] Update Billing-page-coming-soon.md Added page details related to billing and discounts coming from https://github.com/Expensify/Expensify/issues/420340 --- .../Billing-page-coming-soon.md | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md index 2ae2fcd2426d..02edd95f845b 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md @@ -1,6 +1,49 @@ --- title: Billing and Subscriptions -description: Coming soon +description: Billing Page Overview --- -# Coming Soon +# Billing Overview + +At the beginning of each month, the Billing Owner for the workspace will be billed for the previous month’s activity. +Your Expensify bill is determined by: +The number of active members in your workspace +- Whether you have a Collect or Control plan +- Whether you’re on pay-per-use or an annual subscription +- Whether you’re using the Expensify Visa® Commercial Card +- Active members + +An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automated settings. + +## Collect vs Control plan + +Control plan coming soon, comparison to follow. + +## Pay-per-use vs annual subscription + +**Pay-per-use** + +With the pay-per-use rate, you pay the full rate per active member. +- **Collect plan:** $20 per active member +- **Control plan:** $36 per active member + +**Annual** + +With the annual rate, you set your monthly active member count at the beginning of your subscription and get 50% off your monthly active member cost. +- **Collect plan:** $10 per active member +- **Control plan:** $18 per active member + +If you have any additional active members above the number included in your set member count, they will be billed at the pay-per-use rate. You can also choose to increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. For questions, contact Concierge or your account manager. + +## The Expensify Card + +Bundling the Expensify Card with an annual subscription provides you with the lowest monthly price for Expensify. And the more you spend with the Expensify Cards, the lower your bill will be. + +If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per active member when paired with the annual subscription. +- **Collect plan:** $5 per active member +- **Control plan:** $9 per active member + +Additionally, you receive cash back every month that is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. +- 1% cash back on all Expensify Card purchases +- 2% cash back if the amount spent across your Expensify Cards is $250k or more (for U.S. purchases only) + From 7d69e6634b3ac8b61303f1c3ecf06a768a81f4be Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Sun, 6 Oct 2024 01:29:38 +0700 Subject: [PATCH 002/533] Change behavior, navigate before transaction delete to resolve [delete..] briefly appeared --- src/libs/actions/IOU.ts | 50 +++++++++++++++++++++++ src/libs/actions/Task.ts | 1 + src/pages/ReportDetailsPage.tsx | 72 +++++++++++++++++++++------------ 3 files changed, 98 insertions(+), 25 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8d8e25a3ffb6..4b2c4dcd199e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -73,6 +73,7 @@ import * as Category from './Policy/Category'; import * as Policy from './Policy/Policy'; import * as Tag from './Policy/Tag'; import * as Report from './Report'; +import * as Task from '@userActions/Task'; type IOURequestType = ValueOf; @@ -5684,6 +5685,51 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT }; } +function getUrlToNavigateBackForTask(report: OnyxEntry): string | undefined { + const parentReport = Task.getParentReport(report); + const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(report.reportID ?? '-1'); + if (shouldDeleteTaskReport) { + return ROUTES.REPORT_WITH_ID.getRoute(parentReport?.reportID ?? ''); + } + return undefined; +} +function getUrlToNavigateBackForMoneyRequest(transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView: boolean): string | undefined { + const { + shouldDeleteTransactionThread, + shouldDeleteIOUReport, + iouReport, + chatReport, + } = prepareToCleanUpMoneyRequest(transactionID, reportAction, isSingleTransactionView); + + let reportIDToNavigateBack: string | undefined; + if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { + reportIDToNavigateBack = iouReport.chatReportID; + } + + if (iouReport?.chatReportID && shouldDeleteIOUReport) { + reportIDToNavigateBack = iouReport.chatReportID; + } + + return reportIDToNavigateBack ? ROUTES.REPORT_WITH_ID.getRoute(reportIDToNavigateBack) : undefined; +} +function getUrlToNavigateBackForTrackExpense(chatReportID: string, transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView: boolean): string | undefined { + const chatReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; + if (!ReportUtils.isSelfDM(chatReport)) { + return getUrlToNavigateBackForMoneyRequest(transactionID, reportAction, isSingleTransactionView); + } + + const { shouldDeleteTransactionThread } = getDeleteTrackExpenseInformation( + chatReportID, + transactionID, + reportAction, + ); + + if (shouldDeleteTransactionThread) { + return ROUTES.REPORT_WITH_ID.getRoute(chatReportID); + } + + return undefined; +} /** * * @param transactionID - The transactionID of IOU @@ -8476,5 +8522,9 @@ export { updateMoneyRequestTaxRate, mergeDuplicates, resolveDuplicates, + getUrlToNavigateBackForTask, + getUrlToNavigateBackForMoneyRequest, + getUrlToNavigateBackForTrackExpense, + }; export type {GPSPoint as GpsPoint, IOURequestType}; diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 2f9ec060c1e8..ae3c2b596a7d 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1207,6 +1207,7 @@ export { canModifyTask, canActionTask, setNewOptimisticAssignee, + getParentReport, }; export type {PolicyValue, Assignee, ShareDestination}; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e51b8e36704a..03c911305feb 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -718,24 +718,46 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { const deleteTransaction = useCallback(() => { setIsDeleteModalVisible(false); - + + let urlToNavigateBack: string | undefined; + if (caseID === CASES.DEFAULT) { - navigateBackToAfterDelete.current = Task.deleteTask(report); + urlToNavigateBack = IOU.getUrlToNavigateBackForTask(report); + if (urlToNavigateBack) { + Navigation.goBack(urlToNavigateBack as Route); + } else { + Navigation.dismissModal(); + } + Task.deleteTask(report); return; } - + if (!requestParentReportAction) { return; } - - if (ReportActionsUtils.isTrackExpenseAction(requestParentReportAction)) { - navigateBackToAfterDelete.current = IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); + + const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction); + if (isTrackExpense) { + urlToNavigateBack = IOU.getUrlToNavigateBackForTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); } else { - navigateBackToAfterDelete.current = IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); + urlToNavigateBack = IOU.getUrlToNavigateBackForMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); } - + + if (!urlToNavigateBack) { + Navigation.dismissModal(); + } else { + ReportUtils.navigateBackAfterDeleteTransaction(urlToNavigateBack as Route, true); + } + + if (isTrackExpense) { + IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); + } else { + IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); + } + isTransactionDeleted.current = true; }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView]); + return ( @@ -826,23 +848,23 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { onConfirm={deleteTransaction} onCancel={() => setIsDeleteModalVisible(false)} onModalHide={() => { - // We use isTransactionDeleted to know if the modal hides because the user deletes the transaction. - if (!isTransactionDeleted.current) { - if (caseID === CASES.DEFAULT) { - if (navigateBackToAfterDelete.current) { - Navigation.goBack(navigateBackToAfterDelete.current); - } else { - Navigation.dismissModal(); - } - } - return; - } - - if (!navigateBackToAfterDelete.current) { - Navigation.dismissModal(); - } else { - ReportUtils.navigateBackAfterDeleteTransaction(navigateBackToAfterDelete.current, true); - } + // // We use isTransactionDeleted to know if the modal hides because the user deletes the transaction. + // if (!isTransactionDeleted.current) { + // if (caseID === CASES.DEFAULT) { + // if (navigateBackToAfterDelete.current) { + // Navigation.goBack(navigateBackToAfterDelete.current); + // } else { + // Navigation.dismissModal(); + // } + // } + // return; + // } + + // if (!navigateBackToAfterDelete.current) { + // Navigation.dismissModal(); + // } else { + // ReportUtils.navigateBackAfterDeleteTransaction(navigateBackToAfterDelete.current, true); + // } }} prompt={caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1})} confirmText={translate('common.delete')} From bc8f8edbd608afeb42020a776c2886b55c18fae4 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Sun, 6 Oct 2024 23:46:32 +0700 Subject: [PATCH 003/533] fix incorrect reportIDToNavigateBack --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4b2c4dcd199e..9304b3a4c08a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5703,7 +5703,7 @@ function getUrlToNavigateBackForMoneyRequest(transactionID: string, reportAction let reportIDToNavigateBack: string | undefined; if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { - reportIDToNavigateBack = iouReport.chatReportID; + reportIDToNavigateBack = iouReport.reportID; } if (iouReport?.chatReportID && shouldDeleteIOUReport) { From f6781e6e0f0cd6325759f3c6eb64ab8f8790495c Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 7 Oct 2024 00:08:15 +0700 Subject: [PATCH 004/533] actually delete the transaction after ReportDetailsPage unmounted --- src/pages/ReportDetailsPage.tsx | 34 +++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 03c911305feb..600b14117297 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -715,9 +715,35 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { const isTransactionDeleted = useRef(false); // Where to go back after deleting the transaction and its report. It's empty if the transaction report isn't deleted. const navigateBackToAfterDelete = useRef(); + useEffect(() => { + return () => { + if(!isTransactionDeleted.current) + { + return; + } + + if (caseID === CASES.DEFAULT) { + Task.deleteTask(report); + return; + } + + if (!requestParentReportAction) { + return; + } + + const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction); + + if (isTrackExpense) { + IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); + } else { + IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); + } + }; + }, [isTransactionDeleted]); const deleteTransaction = useCallback(() => { setIsDeleteModalVisible(false); + isTransactionDeleted.current = true; let urlToNavigateBack: string | undefined; @@ -728,7 +754,6 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { } else { Navigation.dismissModal(); } - Task.deleteTask(report); return; } @@ -749,13 +774,6 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { ReportUtils.navigateBackAfterDeleteTransaction(urlToNavigateBack as Route, true); } - if (isTrackExpense) { - IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); - } else { - IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); - } - - isTransactionDeleted.current = true; }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView]); return ( From 0bd45cc23e6c040a86e249353ab4da429efbfddb Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 7 Oct 2024 13:46:53 +0700 Subject: [PATCH 005/533] Refactor, remove unnecessary code --- src/pages/ReportDetailsPage.tsx | 75 +++++++++++++-------------------- 1 file changed, 29 insertions(+), 46 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 600b14117297..533709ff50d7 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -3,7 +3,7 @@ import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -713,35 +713,37 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { // A flag to indicate whether the user choose to delete the transaction or not const isTransactionDeleted = useRef(false); - // Where to go back after deleting the transaction and its report. It's empty if the transaction report isn't deleted. - const navigateBackToAfterDelete = useRef(); useEffect(() => { return () => { - if(!isTransactionDeleted.current) - { - return; - } - - if (caseID === CASES.DEFAULT) { - Task.deleteTask(report); - return; - } - - if (!requestParentReportAction) { - return; - } - - const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction); - - if (isTrackExpense) { - IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); - } else { - IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); - } + deleteTransaction(); }; - }, [isTransactionDeleted]); + }, []); const deleteTransaction = useCallback(() => { + if (!isTransactionDeleted.current) { + return; + } + + if (caseID === CASES.DEFAULT) { + Task.deleteTask(report); + return; + } + + if (!requestParentReportAction) { + return; + } + + const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction); + + if (isTrackExpense) { + IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); + } else { + IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); + } + }, [caseID, iouTransactionID, isSingleTransactionView, moneyRequestReport?.reportID, report, requestParentReportAction]); + + // Where to go back after deleting the transaction and its report. + const navigateAfterTransactionDeletion = useCallback(() => { setIsDeleteModalVisible(false); isTransactionDeleted.current = true; @@ -774,7 +776,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { ReportUtils.navigateBackAfterDeleteTransaction(urlToNavigateBack as Route, true); } - }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView]); + }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView, setIsDeleteModalVisible, isTransactionDeleted]); return ( @@ -863,27 +865,8 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { setIsDeleteModalVisible(false)} - onModalHide={() => { - // // We use isTransactionDeleted to know if the modal hides because the user deletes the transaction. - // if (!isTransactionDeleted.current) { - // if (caseID === CASES.DEFAULT) { - // if (navigateBackToAfterDelete.current) { - // Navigation.goBack(navigateBackToAfterDelete.current); - // } else { - // Navigation.dismissModal(); - // } - // } - // return; - // } - - // if (!navigateBackToAfterDelete.current) { - // Navigation.dismissModal(); - // } else { - // ReportUtils.navigateBackAfterDeleteTransaction(navigateBackToAfterDelete.current, true); - // } - }} prompt={caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1})} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} From eb051bb80bce29294ea1552eb62163bd01fda5c1 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 7 Oct 2024 14:23:17 +0700 Subject: [PATCH 006/533] Add NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL to show loading transaction during delete process --- src/ONYXKEYS.ts | 4 ++++ src/libs/actions/IOU.ts | 16 ++++++++++++++++ src/libs/actions/Task.ts | 5 +++++ src/pages/ReportDetailsPage.tsx | 2 ++ src/pages/home/ReportScreen.tsx | 4 +++- 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index df1413620c20..ecc3d865c9e4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -208,6 +208,9 @@ const ONYXKEYS = { /** The NVP containing all information related to educational tooltip in workspace chat */ NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', + /** The NVP contain url to back after deleting transaction */ + NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL: 'nvp_deleteTransactionNavigateBackURL', + /** Whether to show save search rename tooltip */ SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP: 'shouldShowSavedSearchRenameTooltip', @@ -990,6 +993,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; + [ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL]: string | undefined; [ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP]: boolean; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE]: string; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9304b3a4c08a..afaf4b3143b5 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5977,6 +5977,12 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor }); } + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, + value: null, + }); + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -6260,6 +6266,15 @@ function getSendMoneyParams( [optimisticCreatedActionForTransactionThread?.reportActionID ?? '-1']: optimisticCreatedActionForTransactionThread, }, }; + const optimisticDeleteTransactionNavigateBackUrl: OnyxUpdate = { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, + value: null, +}; + + + + const successData: OnyxUpdate[] = []; @@ -6430,6 +6445,7 @@ function getSendMoneyParams( optimisticTransactionData, optimisticTransactionThreadData, optimisticTransactionThreadReportActionsData, + optimisticDeleteTransactionNavigateBackUrl, ]; if (!isEmptyObject(optimisticPersonalDetailListData)) { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index ae3c2b596a7d..b8edbaa8b245 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1005,6 +1005,11 @@ function deleteTask(report: OnyxEntry) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`, value: optimisticReportActions as OnyxTypes.ReportActions, }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, + value: null, + }, ]; // Update optimistic data for parent report action if the report is a child report and the task report has no visible child diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 533709ff50d7..3e098e20b27d 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -751,6 +751,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { if (caseID === CASES.DEFAULT) { urlToNavigateBack = IOU.getUrlToNavigateBackForTask(report); + Onyx.set(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, urlToNavigateBack); if (urlToNavigateBack) { Navigation.goBack(urlToNavigateBack as Route); } else { @@ -769,6 +770,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { } else { urlToNavigateBack = IOU.getUrlToNavigateBackForMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); } + Onyx.set(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, urlToNavigateBack); if (!urlToNavigateBack) { Navigation.dismissModal(); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index dd38a0716377..ac7418062de7 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -343,13 +343,15 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro [currentUserAccountID, linkedAction], ); + const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL); + /** * Using logical OR operator because with nullish coalescing operator, when `isLoadingApp` is false, the right hand side of the operator * is not evaluated. This causes issues where we have `isLoading` set to false and later set to true and then set to false again. * Ideally, `isLoading` should be set initially to true and then set to false. We can achieve this by using logical OR operator. */ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const isLoading = isLoadingApp || !reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty(); + const isLoading = isLoadingApp || !reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty() || (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID); const shouldShowSkeleton = (isLinkingToMessage && !isLinkedMessagePageReady) || From c2cecdc0da807aed28640c29fa411e37a3da8ab1 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 7 Oct 2024 14:33:52 +0700 Subject: [PATCH 007/533] prettier --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index afaf4b3143b5..f8c10e21b006 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -6270,7 +6270,7 @@ function getSendMoneyParams( onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, value: null, -}; + }; From 7e8d4f45dc89f0000da7969d918fce0251a1a8a9 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 7 Oct 2024 15:46:26 +0700 Subject: [PATCH 008/533] Refactor --- src/pages/ReportDetailsPage.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 3e098e20b27d..e82fdf97c1ae 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -715,15 +715,15 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { const isTransactionDeleted = useRef(false); useEffect(() => { return () => { + if (!isTransactionDeleted.current) { + return; + } + deleteTransaction(); }; }, []); const deleteTransaction = useCallback(() => { - if (!isTransactionDeleted.current) { - return; - } - if (caseID === CASES.DEFAULT) { Task.deleteTask(report); return; @@ -743,7 +743,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { }, [caseID, iouTransactionID, isSingleTransactionView, moneyRequestReport?.reportID, report, requestParentReportAction]); // Where to go back after deleting the transaction and its report. - const navigateAfterTransactionDeletion = useCallback(() => { + const navigateToTargetUrl = useCallback(() => { setIsDeleteModalVisible(false); isTransactionDeleted.current = true; @@ -867,7 +867,7 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { setIsDeleteModalVisible(false)} prompt={caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1})} confirmText={translate('common.delete')} From 66e3de8f4bff7c7b1f0990e9efec698dfcea9f7e Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 7 Oct 2024 16:35:27 +0700 Subject: [PATCH 009/533] Partially DeleteTransactionThread to prevent not found page briefly appear --- src/libs/actions/IOU.ts | 46 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index f8c10e21b006..ef2cc894c82b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1744,7 +1744,16 @@ function getDeleteTrackExpenseInformation( { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, - value: null, + value: { + reportID: null, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + participants: { + [userAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + }, }, { onyxMethod: Onyx.METHOD.SET, @@ -1784,6 +1793,17 @@ function getDeleteTrackExpenseInformation( }, ]; + if (shouldDeleteTransactionThread && transactionThread) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: Object.keys(transactionThread).reduce>((acc, key) => { + acc[key] = null; + return acc; + }, {}), + }); + } + const failureData: OnyxUpdate[] = []; if (shouldDeleteTransactionFromOnyx) { @@ -5908,9 +5928,18 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor if (shouldDeleteTransactionThread) { optimisticData.push( { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, - value: null, + value: { + reportID: null, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + participants: { + [userAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + }, }, { onyxMethod: Onyx.METHOD.SET, @@ -6007,6 +6036,17 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor }, ]; + if (shouldDeleteTransactionThread && transactionThread) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: Object.keys(transactionThread).reduce>((acc, key) => { + acc[key] = null; + return acc; + }, {}), + }); + } + if (shouldDeleteIOUReport) { successData.push({ onyxMethod: Onyx.METHOD.SET, From c7e3f5a133d9359e9c1074086fca073fcbd50e3c Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 9 Oct 2024 22:54:21 +0300 Subject: [PATCH 010/533] fix the bottom line of the composer on composer full size mode change --- src/components/Composer/index.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 15087193a593..8a899d4e3562 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -102,6 +102,7 @@ function Composer( const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); + const [prevHeight, setPrevHeight] = useState(); const isReportFlatListScrolling = useRef(false); useEffect(() => { @@ -231,10 +232,18 @@ function Composer( } setPrevScroll(textInput.current.scrollTop); }, 100); + const debouncedSetPrevHeight = lodashDebounce(() => { + if (!textInput.current) { + return; + } + setPrevHeight(textInput.current.clientHeight); + }, 100); textInput.current.addEventListener('scroll', debouncedSetPrevScroll); + textInput.current.addEventListener('resize', debouncedSetPrevHeight); return () => { textInput.current?.removeEventListener('scroll', debouncedSetPrevScroll); + textInput.current?.removeEventListener('resize', debouncedSetPrevHeight); }; }, []); @@ -262,11 +271,11 @@ function Composer( }, []); useEffect(() => { - if (!textInput.current || prevScroll === undefined) { + if (!textInput.current || prevScroll === undefined || prevHeight === undefined) { return; } // eslint-disable-next-line react-compiler/react-compiler - textInput.current.scrollTop = prevScroll; + textInput.current.scrollTop = prevScroll + prevHeight - textInput.current.clientHeight; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isComposerFullSize]); From 26926f016bba9f99ca73b511ef3983db6ed77e35 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 9 Oct 2024 23:03:30 +0300 Subject: [PATCH 011/533] change listener --- src/components/Composer/index.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 8a899d4e3562..56a0a06e9c30 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -231,19 +231,12 @@ function Composer( return; } setPrevScroll(textInput.current.scrollTop); - }, 100); - const debouncedSetPrevHeight = lodashDebounce(() => { - if (!textInput.current) { - return; - } setPrevHeight(textInput.current.clientHeight); }, 100); textInput.current.addEventListener('scroll', debouncedSetPrevScroll); - textInput.current.addEventListener('resize', debouncedSetPrevHeight); return () => { textInput.current?.removeEventListener('scroll', debouncedSetPrevScroll); - textInput.current?.removeEventListener('resize', debouncedSetPrevHeight); }; }, []); From 447dc037c2e6f26b82f305160e7bd005559b2a48 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 9 Oct 2024 23:22:39 +0300 Subject: [PATCH 012/533] set prevHeight to content size height --- src/components/Composer/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 56a0a06e9c30..21708d85612b 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -231,7 +231,6 @@ function Composer( return; } setPrevScroll(textInput.current.scrollTop); - setPrevHeight(textInput.current.clientHeight); }, 100); textInput.current.addEventListener('scroll', debouncedSetPrevScroll); @@ -395,6 +394,7 @@ function Composer( {...props} onSelectionChange={addCursorPositionToSelectionChange} onContentSizeChange={(e) => { + setPrevHeight(e.nativeEvent.contentSize.height); setTextInputWidth(`${e.nativeEvent.contentSize.width}px`); updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); }} From 30335e4905c557ee9a5e7f96f3b01c85e978ab5a Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Thu, 10 Oct 2024 14:23:09 +0200 Subject: [PATCH 013/533] fix(ci): remove `fetch-depth` paramater --- .github/workflows/reassurePerformanceTests.yml | 18 ------------------ .github/workflows/sendReassurePerfData.yml | 2 -- src/App.tsx | 2 ++ 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index fb7a34d6fa01..ed5803c35b42 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -13,8 +13,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup NodeJS uses: ./.github/actions/composite/setupNode @@ -24,22 +22,6 @@ jobs: git config --global user.email "test@test.com" git config --global user.name "Test" - - name: Get common ancestor commit - run: | - git fetch origin main - common_ancestor=$(git merge-base "${{ github.sha }}" origin/main) - echo "COMMIT_HASH=$common_ancestor" >> "$GITHUB_ENV" - - - name: Clean up deleted files - run: | - DELETED_FILES=$(git diff --name-only --diff-filter=D "$COMMIT_HASH" "${{ github.sha }}") - for file in $DELETED_FILES; do - if [ -n "$file" ]; then - rm -f "$file" - echo "Deleted file: $file" - fi - done - - name: Run performance testing script shell: bash run: | diff --git a/.github/workflows/sendReassurePerfData.yml b/.github/workflows/sendReassurePerfData.yml index 884182bfc896..6ae528557faf 100644 --- a/.github/workflows/sendReassurePerfData.yml +++ b/.github/workflows/sendReassurePerfData.yml @@ -12,8 +12,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Setup NodeJS uses: ./.github/actions/composite/setupNode diff --git a/src/App.tsx b/src/App.tsx index 177cc00c7dee..37d1875233d2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,6 +52,8 @@ LogBox.ignoreLogs([ 'Setting a timer for a long period of time', ]); +// trigger action comment + const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; From 57cbb28278fdf353fc4d3067b2618c2f0e1f5c58 Mon Sep 17 00:00:00 2001 From: szymonrybczak Date: Thu, 10 Oct 2024 14:45:02 +0200 Subject: [PATCH 014/533] fix: remove trigger action comment --- src/App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 37d1875233d2..177cc00c7dee 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -52,8 +52,6 @@ LogBox.ignoreLogs([ 'Setting a timer for a long period of time', ]); -// trigger action comment - const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; From 63ee178d2588f50eecc90f54f87c19d3d4105a21 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 16 Oct 2024 11:29:59 +0700 Subject: [PATCH 015/533] fix: Not here page when create new request --- src/libs/actions/IOU.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0f974566a98b..dad6dcd31c46 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7481,6 +7481,13 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + iouReportID: chatReport?.iouReportID ? chatReport?.iouReportID : expenseReport.reportID, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, From 40424321137ae3db8e1151c6fb2112a12fd90c2b Mon Sep 17 00:00:00 2001 From: Stevie LaFortune Date: Wed, 16 Oct 2024 14:04:31 -0500 Subject: [PATCH 016/533] Update docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md Co-authored-by: Carlos Alvarez --- .../billing-and-subscriptions/Billing-page-coming-soon.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md index 02edd95f845b..03e6fb42a6da 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md @@ -7,7 +7,7 @@ description: Billing Page Overview At the beginning of each month, the Billing Owner for the workspace will be billed for the previous month’s activity. Your Expensify bill is determined by: -The number of active members in your workspace +- The number of active members in your workspace - Whether you have a Collect or Control plan - Whether you’re on pay-per-use or an annual subscription - Whether you’re using the Expensify Visa® Commercial Card From 9fd4343b4ca3451180f1c5477edf84da0d289bd2 Mon Sep 17 00:00:00 2001 From: Stevie LaFortune Date: Wed, 16 Oct 2024 15:52:42 -0500 Subject: [PATCH 017/533] Update Billing-page-coming-soon.md --- .../billing-and-subscriptions/Billing-page-coming-soon.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md index 03e6fb42a6da..63c44247658e 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md @@ -13,7 +13,7 @@ Your Expensify bill is determined by: - Whether you’re using the Expensify Visa® Commercial Card - Active members -An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automated settings. +An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automatic actions by Concierge. ## Collect vs Control plan From a3ee0881257c6d5248b61042f8256d86a9c8e6dc Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 21 Oct 2024 10:26:57 +0700 Subject: [PATCH 018/533] fix: logic merge chat report --- src/libs/actions/IOU.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 6c94d36a9e19..8df3cd913dac 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7489,6 +7489,10 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O const stateNum: ValueOf = approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.APPROVED; const statusNum: ValueOf = approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL ? CONST.REPORT.STATUS_NUM.CLOSED : CONST.REPORT.STATUS_NUM.APPROVED; const optimisticNextStep = NextStepUtils.buildNextStep(expenseReport, statusNum); + const iouReportActions = ReportActionsUtils.getAllReportActions(chatReport.iouReportID ?? '-1'); + const expenseReportActions = ReportActionsUtils.getAllReportActions(expenseReport.reportID ?? '-1'); + const iouCreatedAction = Object.values(iouReportActions).find((action) => ReportActionsUtils.isCreatedAction(action)); + const expenseCreatedAction = Object.values(expenseReportActions).find((action) => ReportActionsUtils.isCreatedAction(action)); const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -7504,7 +7508,7 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, value: { - iouReportID: chatReport?.iouReportID ? chatReport?.iouReportID : expenseReport.reportID, + iouReportID: (iouCreatedAction?.created ?? '') > (expenseCreatedAction?.created ?? '') ? chatReport?.iouReportID : expenseReport.reportID, }, }, { From a0617684a9eebdd6f38ecf132d58685a37667748 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 22 Oct 2024 01:49:20 +0530 Subject: [PATCH 019/533] fix height being cut for popover --- src/pages/settings/Security/SecuritySettingsPage.tsx | 11 +++-------- src/styles/variables.ts | 1 + 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx index d1919f9ddf83..924c3c6c468c 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.tsx +++ b/src/pages/settings/Security/SecuritySettingsPage.tsx @@ -47,7 +47,7 @@ function SecuritySettingsPage() { const waitForNavigate = useWaitForNavigation(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {canUseNewDotCopilot} = usePermissions(); - const {windowWidth} = useWindowDimensions(); + const {windowWidth, windowHeight} = useWindowDimensions(); const personalDetails = usePersonalDetails(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); @@ -58,8 +58,6 @@ function SecuritySettingsPage() { const [selectedDelegate, setSelectedDelegate] = useState(); const [anchorPosition, setAnchorPosition] = useState({ - anchorPositionHorizontal: 0, - anchorPositionVertical: 0, anchorPositionTop: 0, anchorPositionRight: 0, }); @@ -70,15 +68,12 @@ function SecuritySettingsPage() { } const position = getClickedTargetLocation(delegateButtonRef.current); - setAnchorPosition({ - anchorPositionTop: position.top + position.height - variables.bankAccountActionPopoverTopSpacing, + anchorPositionTop: Math.min(position.top + position.height - variables.bankAccountActionPopoverTopSpacing, windowHeight - variables.delegateAccessLevelModalHeight), // We want the position to be 23px to the right of the left border anchorPositionRight: windowWidth - position.right + variables.bankAccountActionPopoverRightSpacing, - anchorPositionHorizontal: position.x + variables.addBankAccountLeftSpacing, - anchorPositionVertical: position.y, }); - }, [windowWidth]); + }, [windowWidth, windowHeight, delegateButtonRef]); const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; const delegates = account?.delegatedAccess?.delegates ?? []; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index dc6655791489..ad40c6688eed 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -259,6 +259,7 @@ export default { minimalTopBarOffset: -26, searchHeaderHeight: 80, searchListContentMarginTop: 116, + delegateAccessLevelModalHeight: 168, h20: 20, h28: 28, From fdbb357e1585e7bc98f05b6a5c23a368c461262f Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Tue, 22 Oct 2024 03:30:25 +0530 Subject: [PATCH 020/533] use popovermenu to set anchor position --- .../Security/SecuritySettingsPage.tsx | 94 ++++++++++--------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx index 924c3c6c468c..764412b236ff 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.tsx +++ b/src/pages/settings/Security/SecuritySettingsPage.tsx @@ -14,7 +14,8 @@ import MenuItem from '@components/MenuItem'; import type {MenuItemProps} from '@components/MenuItem'; import MenuItemList from '@components/MenuItemList'; import {usePersonalDetails} from '@components/OnyxProvider'; -import Popover from '@components/Popover'; +import PopoverMenu from '@components/PopoverMenu'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; @@ -32,7 +33,7 @@ import getClickedTargetLocation from '@libs/getClickedTargetLocation'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; -import variables from '@styles/variables'; +import type {AnchorPosition} from '@styles/index'; import * as Modal from '@userActions/Modal'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -47,7 +48,7 @@ function SecuritySettingsPage() { const waitForNavigate = useWaitForNavigation(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {canUseNewDotCopilot} = usePermissions(); - const {windowWidth, windowHeight} = useWindowDimensions(); + const {windowWidth} = useWindowDimensions(); const personalDetails = usePersonalDetails(); const [account] = useOnyx(ONYXKEYS.ACCOUNT); @@ -57,11 +58,18 @@ function SecuritySettingsPage() { const [shouldShowRemoveDelegateModal, setShouldShowRemoveDelegateModal] = useState(false); const [selectedDelegate, setSelectedDelegate] = useState(); - const [anchorPosition, setAnchorPosition] = useState({ - anchorPositionTop: 0, - anchorPositionRight: 0, + const [anchorPosition, setAnchorPosition] = useState({ + horizontal: 0, + vertical: 0, }); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const delegates = account?.delegatedAccess?.delegates ?? []; + const delegators = account?.delegatedAccess?.delegators ?? []; + + const hasDelegates = delegates.length > 0; + const hasDelegators = delegators.length > 0; + const setMenuPosition = useCallback(() => { if (!delegateButtonRef.current) { return; @@ -69,18 +77,10 @@ function SecuritySettingsPage() { const position = getClickedTargetLocation(delegateButtonRef.current); setAnchorPosition({ - anchorPositionTop: Math.min(position.top + position.height - variables.bankAccountActionPopoverTopSpacing, windowHeight - variables.delegateAccessLevelModalHeight), - // We want the position to be 23px to the right of the left border - anchorPositionRight: windowWidth - position.right + variables.bankAccountActionPopoverRightSpacing, + horizontal: windowWidth - position.x, + vertical: position.y + position.height, }); - }, [windowWidth, windowHeight, delegateButtonRef]); - const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; - - const delegates = account?.delegatedAccess?.delegates ?? []; - const delegators = account?.delegatedAccess?.delegators ?? []; - - const hasDelegates = delegates.length > 0; - const hasDelegators = delegators.length > 0; + }, [windowWidth, delegateButtonRef]); const showPopoverMenu = (nativeEvent: GestureResponderEvent | KeyboardEvent, delegate: Delegate) => { delegateButtonRef.current = nativeEvent?.currentTarget as HTMLDivElement; @@ -172,7 +172,7 @@ function SecuritySettingsPage() { }), // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - [delegates, translate, styles, personalDetails], + [delegates, translate, styles, personalDetails, windowWidth], ); const delegatorMenuItems: MenuItemProps[] = useMemo( @@ -198,6 +198,27 @@ function SecuritySettingsPage() { [delegators, styles, translate, personalDetails], ); + const delegatePopoverMenuItems: PopoverMenuItem[] = [ + { + text: translate('delegate.changeAccessLevel'), + icon: Expensicons.Pencil, + onPress: () => { + Navigation.navigate(ROUTES.SETTINGS_UPDATE_DELEGATE_ROLE.getRoute(selectedDelegate?.email ?? '', selectedDelegate?.role ?? '')); + setShouldShowDelegatePopoverMenu(false); + setSelectedDelegate(undefined); + }, + }, + { + text: translate('delegate.removeCopilot'), + icon: Expensicons.Trashcan, + onPress: () => + Modal.close(() => { + setShouldShowDelegatePopoverMenu(false); + setShouldShowRemoveDelegateModal(true); + }), + }, + ]; + return ( )} - } anchorPosition={{ - top: anchorPosition.anchorPositionTop, - right: anchorPosition.anchorPositionRight, + horizontal: anchorPosition.horizontal, + vertical: anchorPosition.vertical, }} + anchorAlignment={{ + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, + }} + menuItems={delegatePopoverMenuItems} onClose={() => { setShouldShowDelegatePopoverMenu(false); }} - > - - { - Navigation.navigate(ROUTES.SETTINGS_UPDATE_DELEGATE_ROLE.getRoute(selectedDelegate?.email ?? '', selectedDelegate?.role ?? '')); - setShouldShowDelegatePopoverMenu(false); - setSelectedDelegate(undefined); - }} - wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]} - /> - - Modal.close(() => { - setShouldShowDelegatePopoverMenu(false); - setShouldShowRemoveDelegateModal(true); - }) - } - wrapperStyle={[styles.pv3, styles.ph5, !shouldUseNarrowLayout ? styles.sidebarPopover : {}]} - /> - - + /> Date: Tue, 22 Oct 2024 03:40:17 +0530 Subject: [PATCH 021/533] rm unused --- src/styles/variables.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/styles/variables.ts b/src/styles/variables.ts index ad40c6688eed..dc6655791489 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -259,7 +259,6 @@ export default { minimalTopBarOffset: -26, searchHeaderHeight: 80, searchListContentMarginTop: 116, - delegateAccessLevelModalHeight: 168, h20: 20, h28: 28, From ccb9ca5eca10055b51a4f084ba29e2e76d1a2f17 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 22 Oct 2024 16:02:17 +0300 Subject: [PATCH 022/533] resolve conflict --- src/components/Composer/implementation/index.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 4431007793cb..6c4a1a4c723c 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -75,6 +75,7 @@ function Composer( const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); + const [prevHeight, setPrevHeight] = useState(); const isReportFlatListScrolling = useRef(false); useEffect(() => { @@ -243,11 +244,11 @@ function Composer( }, []); useEffect(() => { - if (!textInput.current || prevScroll === undefined) { + if (!textInput.current || prevScroll === undefined || prevHeight === undefined) { return; } // eslint-disable-next-line react-compiler/react-compiler - textInput.current.scrollTop = prevScroll; + textInput.current.scrollTop = prevScroll + prevHeight - textInput.current.clientHeight; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isComposerFullSize]); @@ -353,6 +354,7 @@ function Composer( {...props} onSelectionChange={addCursorPositionToSelectionChange} onContentSizeChange={(e) => { + setPrevHeight(e.nativeEvent.contentSize.height); updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); }} disabled={isDisabled} From 9bce4f03bb5c00cd93be04f841a23d7e54c9dc35 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 23 Oct 2024 21:54:54 +0700 Subject: [PATCH 023/533] decouple urlToNavigateBack functions --- src/libs/actions/IOU.ts | 116 ++++++++++++++++++++++++++++++++++----- src/libs/actions/Task.ts | 53 +++++++++++++++--- 2 files changed, 148 insertions(+), 21 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 497f43f93317..01a86eef2b21 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5713,16 +5713,17 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT // STEP 5: Calculate the url that the user will be navigated back to // This depends on which page they are on and which resources were deleted - let reportIDToNavigateBack: string | undefined; - if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { - reportIDToNavigateBack = iouReport.reportID; - } + // let reportIDToNavigateBack: string | undefined; + // if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { + // reportIDToNavigateBack = iouReport.reportID; + // } - if (iouReport?.chatReportID && shouldDeleteIOUReport) { - reportIDToNavigateBack = iouReport.chatReportID; - } + // if (iouReport?.chatReportID && shouldDeleteIOUReport) { + // reportIDToNavigateBack = iouReport.chatReportID; + // } - const urlToNavigateBack = reportIDToNavigateBack ? ROUTES.REPORT_WITH_ID.getRoute(reportIDToNavigateBack) : undefined; + // const urlToNavigateBack = reportIDToNavigateBack ? ROUTES.REPORT_WITH_ID.getRoute(reportIDToNavigateBack) : undefined; + const urlToNavigateBack = getNavigationUrlAfterMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); return { shouldDeleteTransactionThread, @@ -5741,6 +5742,84 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT }; } +/** + * Calculate the URL to navigate to after a money request deletion + * @param transactionID - The ID of the money request being deleted + * @param reportAction - The report action associated with the money request + * @param isSingleTransactionView - Whether we're in single transaction view + * @returns The URL to navigate to + */ +function getNavigationUrlAfterMoneyRequestDelete( + transactionID: string, + reportAction: OnyxTypes.ReportAction, + isSingleTransactionView = false +): string | undefined { + // Get all collections we need for navigation + const allReports = ReportConnection.getAllReports(); + const iouReportID = ReportActionsUtils.isMoneyRequestAction(reportAction) + ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID + : '-1'; + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`] ?? null; + const transactionThreadID = reportAction.childReportID; + + // Calculate if resources would be deleted + const shouldDeleteTransactionThread = transactionThreadID + ? (reportAction?.childVisibleActionCount ?? 0) === 0 + : false; + + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(iouReport?.reportID ?? '-1'); + const iouReportLastMessageText = ReportActionsUtils.getLastVisibleMessage(iouReport?.reportID ?? '-1').lastMessageText; + const shouldDeleteIOUReport = iouReportLastMessageText.length === 0 + && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) + && (!transactionThreadID || shouldDeleteTransactionThread); + + // Determine which report to navigate back to + if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { + return ROUTES.REPORT_WITH_ID.getRoute(iouReport.reportID); + } + + if (iouReport?.chatReportID && shouldDeleteIOUReport) { + return ROUTES.REPORT_WITH_ID.getRoute(iouReport.chatReportID); + } + + return undefined; +} + +/** + * Calculate the URL to navigate to after a track expense deletion + * @param chatReportID - The ID of the chat report containing the track expense + * @param transactionID - The ID of the track expense being deleted + * @param reportAction - The report action associated with the track expense + * @param isSingleTransactionView - Whether we're in single transaction view + * @returns The URL to navigate to + */ +function getNavigationUrlAfterTrackExpenseDelete( + chatReportID: string, + transactionID: string, + reportAction: OnyxTypes.ReportAction, + isSingleTransactionView = false +): string | undefined { + const chatReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; + + // If not a self DM, handle it as a regular money request + if (!ReportUtils.isSelfDM(chatReport)) { + return getNavigationUrlAfterMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); + } + + const transactionThreadID = reportAction.childReportID; + const shouldDeleteTransactionThread = transactionThreadID + ? (reportAction?.childVisibleActionCount ?? 0) === 0 + : false; + + // Only navigate if in single transaction view and the thread will be deleted + if (isSingleTransactionView && shouldDeleteTransactionThread && chatReport?.reportID) { + // Pop the deleted report screen before navigating. This prevents navigating to the Concierge chat due to the missing report. + return ROUTES.REPORT_WITH_ID.getRoute(chatReport.reportID); + } + + return undefined; +} + /** * * @param transactionID - The transactionID of IOU @@ -6115,10 +6194,18 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor } function deleteTrackExpense(chatReportID: string, transactionID: string, reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false) { + const urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete( + chatReportID, + transactionID, + reportAction, + isSingleTransactionView + ); + // STEP 1: Get all collections we're updating const chatReport = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] ?? null; if (!ReportUtils.isSelfDM(chatReport)) { - return deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView); + deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView); + return urlToNavigateBack; } const whisperAction = ReportActionsUtils.getTrackExpenseActionableWhisper(transactionID, chatReportID); @@ -6138,10 +6225,11 @@ function deleteTrackExpense(chatReportID: string, transactionID: string, reportA 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. - return ROUTES.REPORT_WITH_ID.getRoute(chatReport?.reportID ?? '-1'); - } + return urlToNavigateBack; + // if (isSingleTransactionView && shouldDeleteTransactionThread) { + // // Pop the deleted report screen before navigating. This prevents navigating to the Concierge chat due to the missing report. + // return ROUTES.REPORT_WITH_ID.getRoute(chatReport?.reportID ?? '-1'); + // } } /** @@ -8565,5 +8653,7 @@ export { updateLastLocationPermissionPrompt, resolveDuplicates, getIOUReportActionToApproveOrPay, + getNavigationUrlAfterMoneyRequestDelete, + getNavigationUrlAfterTrackExpenseDelete, }; export type {GPSPoint as GpsPoint, IOURequestType}; diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index b159e391f949..2031d5a42257 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -955,6 +955,36 @@ function getParentReport(report: OnyxEntry): OnyxEntry): string | undefined { + if (!report) { + return undefined; + } + + const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(report.reportID ?? '-1'); + if (!shouldDeleteTaskReport) { + return undefined; + } + + // First try to navigate to parent report + const parentReport = getParentReport(report); + if (parentReport?.reportID) { + return ROUTES.REPORT_WITH_ID.getRoute(parentReport.reportID); + } + + // If no parent report, try to navigate to most recent report + const mostRecentReportID = Report.getMostRecentReportID(report); + if (mostRecentReportID) { + return ROUTES.REPORT_WITH_ID.getRoute(mostRecentReportID); + } + + return undefined; +} + /** * Cancels a task by setting the report state to SUBMITTED and status to CLOSED */ @@ -1109,15 +1139,21 @@ function deleteTask(report: OnyxEntry) { API.write(WRITE_COMMANDS.CANCEL_TASK, parameters, {optimisticData, successData, failureData}); Report.notifyNewAction(report.reportID, currentUserAccountID); - if (shouldDeleteTaskReport) { + // if (shouldDeleteTaskReport) { + // Navigation.goBack(); + // if (parentReport?.reportID) { + // return ROUTES.REPORT_WITH_ID.getRoute(parentReport.reportID); + // } + // const mostRecentReportID = Report.getMostRecentReportID(report); + // if (mostRecentReportID) { + // return ROUTES.REPORT_WITH_ID.getRoute(mostRecentReportID); + // } + // } + + const urlToNavigateBack = getNavigationUrlAfterTaskDelete(report); + if (urlToNavigateBack) { Navigation.goBack(); - if (parentReport?.reportID) { - return ROUTES.REPORT_WITH_ID.getRoute(parentReport.reportID); - } - const mostRecentReportID = Report.getMostRecentReportID(report); - if (mostRecentReportID) { - return ROUTES.REPORT_WITH_ID.getRoute(mostRecentReportID); - } + return urlToNavigateBack; } } @@ -1232,6 +1268,7 @@ export { canActionTask, setNewOptimisticAssignee, getParentReport, + getNavigationUrlAfterTaskDelete, }; export type {PolicyValue, Assignee, ShareDestination}; From e2cd64213c51a03b9d0a0836257d82430899e4b4 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 23 Oct 2024 22:06:39 +0700 Subject: [PATCH 024/533] actually delete transaction after reportdetailspage unmounted --- src/pages/ReportDetailsPage.tsx | 107 +++++++++++++++++++++++--------- 1 file changed, 78 insertions(+), 29 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 7de12eeda892..fcaa696d526b 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -3,7 +3,7 @@ import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -225,6 +225,18 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { const canUnapproveRequest = ReportUtils.isExpenseReport(report) && (ReportUtils.isReportManager(report) || isPolicyAdmin) && ReportUtils.isReportApproved(report) && !PolicyUtils.isSubmitAndClose(policy); + + useEffect(() => { + return () => { + if (!isTransactionDeleted.current) { + return; + } + + deleteTransaction(); + }; + }, []); + + useEffect(() => { if (canDeleteRequest) { return; @@ -759,11 +771,48 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { // Where to go back after deleting the transaction and its report. It's empty if the transaction report isn't deleted. const navigateBackToAfterDelete = useRef(); - const deleteTransaction = useCallback(() => { + + // Where to go back after deleting the transaction and its report. + const navigateToTargetUrl = useCallback(() => { setIsDeleteModalVisible(false); + isTransactionDeleted.current = true; + + let urlToNavigateBack: string | undefined; + + if (caseID === CASES.DEFAULT) { + urlToNavigateBack = Task.getNavigationUrlAfterTaskDelete(report); + Onyx.set(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, urlToNavigateBack); + if (urlToNavigateBack) { + Navigation.goBack(urlToNavigateBack as Route); + } else { + Navigation.dismissModal(); + } + return; + } + + if (!requestParentReportAction) { + return; + } + + const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction); + if (isTrackExpense) { + urlToNavigateBack = IOU.getNavigationUrlAfterTrackExpenseDelete(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); + } else { + urlToNavigateBack = IOU.getNavigationUrlAfterMoneyRequestDelete(iouTransactionID, requestParentReportAction, isSingleTransactionView); + } + Onyx.set(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, urlToNavigateBack); + + if (!urlToNavigateBack) { + Navigation.dismissModal(); + } else { + ReportUtils.navigateBackAfterDeleteTransaction(urlToNavigateBack as Route, true); + } + + }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView, setIsDeleteModalVisible, isTransactionDeleted]); + const deleteTransaction = useCallback(() => { if (caseID === CASES.DEFAULT) { - navigateBackToAfterDelete.current = Task.deleteTask(report); + Task.deleteTask(report); return; } @@ -771,14 +820,14 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { return; } - if (ReportActionsUtils.isTrackExpenseAction(requestParentReportAction)) { - navigateBackToAfterDelete.current = IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); + const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction); + + if (isTrackExpense) { + IOU.deleteTrackExpense(moneyRequestReport?.reportID ?? '', iouTransactionID, requestParentReportAction, isSingleTransactionView); } else { - navigateBackToAfterDelete.current = IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); + IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); } - - isTransactionDeleted.current = true; - }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView]); + }, [caseID, iouTransactionID, isSingleTransactionView, moneyRequestReport?.reportID, report, requestParentReportAction]); const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); @@ -871,27 +920,27 @@ function ReportDetailsPage({policies, report, route}: ReportDetailsPageProps) { setIsDeleteModalVisible(false)} - onModalHide={() => { - // We use isTransactionDeleted to know if the modal hides because the user deletes the transaction. - if (!isTransactionDeleted.current) { - if (caseID === CASES.DEFAULT) { - if (navigateBackToAfterDelete.current) { - Navigation.goBack(navigateBackToAfterDelete.current); - } else { - Navigation.dismissModal(); - } - } - return; - } - - if (!navigateBackToAfterDelete.current) { - Navigation.dismissModal(); - } else { - ReportUtils.navigateBackAfterDeleteTransaction(navigateBackToAfterDelete.current, true); - } - }} + // onModalHide={() => { + // // We use isTransactionDeleted to know if the modal hides because the user deletes the transaction. + // if (!isTransactionDeleted.current) { + // if (caseID === CASES.DEFAULT) { + // if (navigateBackToAfterDelete.current) { + // Navigation.goBack(navigateBackToAfterDelete.current); + // } else { + // Navigation.dismissModal(); + // } + // } + // return; + // } + + // if (!navigateBackToAfterDelete.current) { + // Navigation.dismissModal(); + // } else { + // ReportUtils.navigateBackAfterDeleteTransaction(navigateBackToAfterDelete.current, true); + // } + // }} prompt={caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1})} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} From dca3bcc040cf5545c1feabb88f63289ae65ce4e5 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Wed, 23 Oct 2024 18:47:15 +0300 Subject: [PATCH 025/533] scroll to the cursor on composer size change for native --- .../Composer/implementation/index.native.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 9f237dd02424..905fa537ff55 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -59,6 +59,17 @@ function Composer( inputCallbackRef(autoFocus ? textInput.current : null); }, [autoFocus, inputCallbackRef, autoFocusInputRef]); + useEffect(() => { + if (!textInput.current || !selection) { + return; + } + + // We are setting selection twice to trigger a scroll to the cursor on change of composer size. + textInput.current?.setSelection((selection.start || 1) - 1, selection.start); + textInput.current?.setSelection(selection.start, selection.start); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [isComposerFullSize]); + /** * Set the TextInput Ref * @param {Element} el From 980e8e5dc810f1e2e511228e2dfc817b89a3034c Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Thu, 24 Oct 2024 04:40:53 +0700 Subject: [PATCH 026/533] Resolve cyclic dependency --- src/libs/actions/IOU.ts | 42 ++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 01a86eef2b21..53a0b4f47e9e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5723,7 +5723,7 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT // } // const urlToNavigateBack = reportIDToNavigateBack ? ROUTES.REPORT_WITH_ID.getRoute(reportIDToNavigateBack) : undefined; - const urlToNavigateBack = getNavigationUrlAfterMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); + const urlToNavigateBack = undefined; return { shouldDeleteTransactionThread, @@ -5754,24 +5754,32 @@ function getNavigationUrlAfterMoneyRequestDelete( reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false ): string | undefined { - // Get all collections we need for navigation - const allReports = ReportConnection.getAllReports(); - const iouReportID = ReportActionsUtils.isMoneyRequestAction(reportAction) - ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID - : '-1'; - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`] ?? null; - const transactionThreadID = reportAction.childReportID; + // // Get all collections we need for navigation + // const allReports = ReportConnection.getAllReports(); + // const iouReportID = ReportActionsUtils.isMoneyRequestAction(reportAction) + // ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID + // : '-1'; + // const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`] ?? null; + // const transactionThreadID = reportAction.childReportID; + + // // Calculate if resources would be deleted + // const shouldDeleteTransactionThread = transactionThreadID + // ? (reportAction?.childVisibleActionCount ?? 0) === 0 + // : false; + + // const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(iouReport?.reportID ?? '-1'); + // const iouReportLastMessageText = ReportActionsUtils.getLastVisibleMessage(iouReport?.reportID ?? '-1').lastMessageText; + // const shouldDeleteIOUReport = iouReportLastMessageText.length === 0 + // && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) + // && (!transactionThreadID || shouldDeleteTransactionThread); - // Calculate if resources would be deleted - const shouldDeleteTransactionThread = transactionThreadID - ? (reportAction?.childVisibleActionCount ?? 0) === 0 - : false; + const { + shouldDeleteTransactionThread, + shouldDeleteIOUReport, + iouReport, + chatReport, + } = prepareToCleanUpMoneyRequest(transactionID, reportAction, isSingleTransactionView); - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(iouReport?.reportID ?? '-1'); - const iouReportLastMessageText = ReportActionsUtils.getLastVisibleMessage(iouReport?.reportID ?? '-1').lastMessageText; - const shouldDeleteIOUReport = iouReportLastMessageText.length === 0 - && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) - && (!transactionThreadID || shouldDeleteTransactionThread); // Determine which report to navigate back to if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { From 0288efb572cf441b44066069362f6a1cc5c880f9 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Thu, 24 Oct 2024 08:58:00 +0700 Subject: [PATCH 027/533] extract navagateUrl from prepareToCleanUpMoneyRequest --- src/libs/actions/IOU.ts | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 53a0b4f47e9e..64dca4fd8517 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5723,7 +5723,7 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT // } // const urlToNavigateBack = reportIDToNavigateBack ? ROUTES.REPORT_WITH_ID.getRoute(reportIDToNavigateBack) : undefined; - const urlToNavigateBack = undefined; + // const urlToNavigateBack = undefined; return { shouldDeleteTransactionThread, @@ -5738,7 +5738,7 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT transactionViolations, reportPreviewAction, iouReport, - urlToNavigateBack, + // urlToNavigateBack, }; } @@ -5754,30 +5754,10 @@ function getNavigationUrlAfterMoneyRequestDelete( reportAction: OnyxTypes.ReportAction, isSingleTransactionView = false ): string | undefined { - // // Get all collections we need for navigation - // const allReports = ReportConnection.getAllReports(); - // const iouReportID = ReportActionsUtils.isMoneyRequestAction(reportAction) - // ? ReportActionsUtils.getOriginalMessage(reportAction)?.IOUReportID - // : '-1'; - // const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`] ?? null; - // const transactionThreadID = reportAction.childReportID; - - // // Calculate if resources would be deleted - // const shouldDeleteTransactionThread = transactionThreadID - // ? (reportAction?.childVisibleActionCount ?? 0) === 0 - // : false; - - // const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(iouReport?.reportID ?? '-1'); - // const iouReportLastMessageText = ReportActionsUtils.getLastVisibleMessage(iouReport?.reportID ?? '-1').lastMessageText; - // const shouldDeleteIOUReport = iouReportLastMessageText.length === 0 - // && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) - // && (!transactionThreadID || shouldDeleteTransactionThread); - const { shouldDeleteTransactionThread, shouldDeleteIOUReport, iouReport, - chatReport, } = prepareToCleanUpMoneyRequest(transactionID, reportAction, isSingleTransactionView); @@ -5846,9 +5826,10 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo chatReport, iouReport, reportPreviewAction, - urlToNavigateBack, } = prepareToCleanUpMoneyRequest(transactionID, reportAction, isSingleTransactionView); + + const urlToNavigateBack = getNavigationUrlAfterMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); // build Onyx data // Onyx operations to delete the transaction, update the IOU report action and chat report action @@ -5984,9 +5965,10 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor transactionViolations, iouReport, reportPreviewAction, - urlToNavigateBack, } = prepareToCleanUpMoneyRequest(transactionID, reportAction, isSingleTransactionView); + const urlToNavigateBack = getNavigationUrlAfterMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); + // STEP 2: Build Onyx data // The logic mostly resembles the cleanUpMoneyRequest function const optimisticData: OnyxUpdate[] = [ From 5d982cc62be7074302d153b1d575c6ec63946d15 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Thu, 24 Oct 2024 09:45:21 +0700 Subject: [PATCH 028/533] Implement NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL --- src/libs/actions/IOU.ts | 10 ++++++++++ src/pages/home/ReportScreen.tsx | 11 +++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c125e0830568..b271baf93886 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1779,6 +1779,11 @@ function getDeleteTrackExpenseInformation( lastMessageHtml: !lastMessageHtml ? lastMessageText : lastMessageHtml, }, }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, + value: null, + } ); const successData: OnyxUpdate[] = [ @@ -6020,6 +6025,11 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, value: ReportUtils.getOutstandingChildRequest(updatedIOUReport), }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, + value: null, + } ); if (!shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index baf3d02d7af3..d17ee485498a 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -342,8 +342,15 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro () => !!linkedAction && ReportActionsUtils.isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), [currentUserAccountID, linkedAction], ); - - const isLoading = isLoadingApp ?? (!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty()); + const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL); + console.log("[wildebug] ~ file: ReportScreen.tsx:346 ~ ReportScreen ~ deleteTransactionNavigateBackUrl:", deleteTransactionNavigateBackUrl) + console.log("[wildebug] ~ file: ReportScreen.tsx:348 ~ ReportScreen ~ report?.reportID:", report?.reportID) + console.log("[wildebug] ~ file: ReportScreen.tsx:348 ~ ReportScreen ~ ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl):", ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl)) + console.log("[wildebug] ~ file: ReportScreen.tsx:348 ~ ReportScreen ~ (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID):", (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID)) + + const isLoading = isLoadingApp || ((!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty()) || (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID)); + // const isLoading = isLoadingApp || ((!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty())); + console.log("[wildebug] ~ file: ReportScreen.tsx:348 ~ ReportScreen ~ isLoading:", isLoading) const shouldShowSkeleton = (isLinkingToMessage && !isLinkedMessagePageReady) || (!isLinkingToMessage && !isInitialPageReady) || From 44473445ee83fc22b70910ca11eb0a147776d03e Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Thu, 24 Oct 2024 10:07:01 +0700 Subject: [PATCH 029/533] change delete money request and track expense to merge instead of fast, to resolve not found page briefly appeared --- src/libs/actions/IOU.ts | 48 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index b271baf93886..45f0a95e424b 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1752,9 +1752,18 @@ function getDeleteTrackExpenseInformation( if (shouldDeleteTransactionThread) { optimisticData.push( { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, - value: null, + value: { + reportID: null, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + participants: { + [userAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + }, }, { onyxMethod: Onyx.METHOD.SET, @@ -1799,6 +1808,17 @@ function getDeleteTrackExpenseInformation( }, ]; + if (shouldDeleteTransactionThread && transactionThread) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: Object.keys(transactionThread).reduce>((acc, key) => { + acc[key] = null; + return acc; + }, {}), + }); + } + const failureData: OnyxUpdate[] = []; if (shouldDeleteTransactionFromOnyx) { @@ -5990,9 +6010,18 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor if (shouldDeleteTransactionThread) { optimisticData.push( { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, - value: null, + value: { + reportID: null, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + participants: { + [userAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + }, }, { onyxMethod: Onyx.METHOD.SET, @@ -6088,6 +6117,17 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor }, ]; + if (shouldDeleteTransactionThread && transactionThread) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: Object.keys(transactionThread).reduce>((acc, key) => { + acc[key] = null; + return acc; + }, {}), + }); + } + if (shouldDeleteIOUReport) { successData.push({ onyxMethod: Onyx.METHOD.SET, From 71c3d417ef110eee161f8c51e5789d77a1ba5762 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Thu, 24 Oct 2024 11:04:06 +0700 Subject: [PATCH 030/533] show skeleton when deleting processed --- src/libs/actions/IOU.ts | 10 ---------- src/libs/actions/Task.ts | 5 ----- src/pages/home/ReportScreen.tsx | 25 +++++++++++++++++-------- 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 45f0a95e424b..39692bc10fe0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1788,11 +1788,6 @@ function getDeleteTrackExpenseInformation( lastMessageHtml: !lastMessageHtml ? lastMessageText : lastMessageHtml, }, }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, - value: null, - } ); const successData: OnyxUpdate[] = [ @@ -6054,11 +6049,6 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, value: ReportUtils.getOutstandingChildRequest(updatedIOUReport), }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, - value: null, - } ); if (!shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 2031d5a42257..b2fc07bd8685 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1053,11 +1053,6 @@ function deleteTask(report: OnyxEntry) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport?.reportID}`, value: optimisticReportActions as OnyxTypes.ReportActions, }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, - value: null, - }, ]; // Update optimistic data for parent report action if the report is a child report and the task report has no visible child diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index d17ee485498a..15c5e9b34877 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -55,6 +55,9 @@ import ReportActionsView from './report/ReportActionsView'; import ReportFooter from './report/ReportFooter'; import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; +import Onyx from 'react-native-onyx'; +import lodashDefer from 'lodash/defer'; + type ReportScreenNavigationProps = StackScreenProps; @@ -343,20 +346,26 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro [currentUserAccountID, linkedAction], ); const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL); - console.log("[wildebug] ~ file: ReportScreen.tsx:346 ~ ReportScreen ~ deleteTransactionNavigateBackUrl:", deleteTransactionNavigateBackUrl) - console.log("[wildebug] ~ file: ReportScreen.tsx:348 ~ ReportScreen ~ report?.reportID:", report?.reportID) - console.log("[wildebug] ~ file: ReportScreen.tsx:348 ~ ReportScreen ~ ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl):", ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl)) - console.log("[wildebug] ~ file: ReportScreen.tsx:348 ~ ReportScreen ~ (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID):", (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID)) - - const isLoading = isLoadingApp || ((!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty()) || (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID)); - // const isLoading = isLoadingApp || ((!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty())); - console.log("[wildebug] ~ file: ReportScreen.tsx:348 ~ ReportScreen ~ isLoading:", isLoading) + + useEffect(() => { + if (!isFocused || !deleteTransactionNavigateBackUrl) { + return; + } + // Schedule the code to run after the current call stack is cleared, + // ensuring all updates are processed before hide the skeleton + lodashDefer(() => { + Onyx.merge(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, null); + }); + }, [isFocused]); + + const isLoading = isLoadingApp ?? ((!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty())); const shouldShowSkeleton = (isLinkingToMessage && !isLinkedMessagePageReady) || (!isLinkingToMessage && !isInitialPageReady) || isEmptyObject(reportOnyx) || isLoadingReportOnyx || !isCurrentReportLoadedFromOnyx || + (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID) || isLoading; const isLinkedActionBecomesDeleted = prevIsLinkedActionDeleted !== undefined && !prevIsLinkedActionDeleted && isLinkedActionDeleted; From 54402d18f585bfc1c34e61502c43d910d505e341 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 28 Oct 2024 18:40:39 +0300 Subject: [PATCH 031/533] comment out the set selection code --- .../Composer/implementation/index.native.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 905fa537ff55..23531ceafa83 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -59,16 +59,16 @@ function Composer( inputCallbackRef(autoFocus ? textInput.current : null); }, [autoFocus, inputCallbackRef, autoFocusInputRef]); - useEffect(() => { - if (!textInput.current || !selection) { - return; - } + // useEffect(() => { + // if (!textInput.current || !selection) { + // return; + // } - // We are setting selection twice to trigger a scroll to the cursor on change of composer size. - textInput.current?.setSelection((selection.start || 1) - 1, selection.start); - textInput.current?.setSelection(selection.start, selection.start); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [isComposerFullSize]); + // // We are setting selection twice to trigger a scroll to the cursor on change of composer size. + // textInput.current?.setSelection((selection.start || 1) - 1, selection.start); + // textInput.current?.setSelection(selection.start, selection.start); + // // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + // }, [isComposerFullSize]); /** * Set the TextInput Ref From 4e65339fec300f16e3ff124bc5c4d1c88b7095d7 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 28 Oct 2024 23:33:16 +0300 Subject: [PATCH 032/533] revert --- .../Composer/implementation/index.native.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 23531ceafa83..905fa537ff55 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -59,16 +59,16 @@ function Composer( inputCallbackRef(autoFocus ? textInput.current : null); }, [autoFocus, inputCallbackRef, autoFocusInputRef]); - // useEffect(() => { - // if (!textInput.current || !selection) { - // return; - // } + useEffect(() => { + if (!textInput.current || !selection) { + return; + } - // // We are setting selection twice to trigger a scroll to the cursor on change of composer size. - // textInput.current?.setSelection((selection.start || 1) - 1, selection.start); - // textInput.current?.setSelection(selection.start, selection.start); - // // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - // }, [isComposerFullSize]); + // We are setting selection twice to trigger a scroll to the cursor on change of composer size. + textInput.current?.setSelection((selection.start || 1) - 1, selection.start); + textInput.current?.setSelection(selection.start, selection.start); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [isComposerFullSize]); /** * Set the TextInput Ref From aa8156ddbb3b807b3226a693289a6df4ad5fa310 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 28 Oct 2024 23:36:09 +0300 Subject: [PATCH 033/533] only scroll to cursor on changing to composer to small size --- src/components/Composer/implementation/index.native.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 905fa537ff55..74c96555b644 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -60,11 +60,11 @@ function Composer( }, [autoFocus, inputCallbackRef, autoFocusInputRef]); useEffect(() => { - if (!textInput.current || !selection) { + if (!textInput.current || !selection || isComposerFullSize) { return; } - // We are setting selection twice to trigger a scroll to the cursor on change of composer size. + // We are setting selection twice to trigger a scroll to the cursor on toggling to smaller composer size. textInput.current?.setSelection((selection.start || 1) - 1, selection.start); textInput.current?.setSelection(selection.start, selection.start); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps From 913563b841946fbbe5bedee861233f19ab9484dc Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 29 Oct 2024 00:30:19 +0300 Subject: [PATCH 034/533] fix tests --- src/components/Composer/implementation/index.native.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 74c96555b644..9f136effa857 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -60,7 +60,7 @@ function Composer( }, [autoFocus, inputCallbackRef, autoFocusInputRef]); useEffect(() => { - if (!textInput.current || !selection || isComposerFullSize) { + if (!textInput.current || !textInput.current.setSelection || !selection || isComposerFullSize) { return; } From 02d6985b9d4c2d49659a8dd22511ab6ce2432a66 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 30 Oct 2024 10:10:27 +0530 Subject: [PATCH 035/533] fix: Provide education/confirmation before creating workspaces in New Workspace flows. Signed-off-by: krishna2323 --- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 1 + src/SCREENS.ts | 3 + src/components/CurrencyPicker.tsx | 84 +++++++++ src/components/ValuePicker/types.ts | 2 + src/libs/CurrencyUtils.ts | 7 + .../ModalStackNavigators/index.tsx | 6 + .../Navigators/RightModalNavigator.tsx | 6 + src/libs/Navigation/linkingConfig/config.ts | 7 + src/libs/Navigation/types.ts | 6 + src/libs/Permissions.ts | 1 + src/libs/SubscriptionUtils.ts | 1 + src/libs/actions/App.ts | 14 +- src/libs/actions/Policy/Policy.ts | 12 +- .../workspace/WorkspaceConfirmationPage.tsx | 169 ++++++++++++++++++ src/pages/workspace/WorkspacesListPage.tsx | 7 +- src/types/form/WorkspaceConfirmationForm.ts | 20 +++ 17 files changed, 338 insertions(+), 11 deletions(-) create mode 100644 src/components/CurrencyPicker.tsx create mode 100644 src/pages/workspace/WorkspaceConfirmationPage.tsx create mode 100644 src/types/form/WorkspaceConfirmationForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index e14e536154a3..9f6776454464 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -3,6 +3,7 @@ import type CONST from './CONST'; import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST'; import type Platform from './libs/getPlatform/types'; import type * as FormTypes from './types/form'; +import type {WorkspaceConfirmationForm} from './types/form/WorkspaceConfirmationForm'; import type * as OnyxTypes from './types/onyx'; import type {Attendee} from './types/onyx/IOU'; import type Onboarding from './types/onyx/Onboarding'; @@ -539,6 +540,7 @@ const ONYXKEYS = { ADD_PAYMENT_CARD_FORM_DRAFT: 'addPaymentCardFormDraft', WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', + WORKSPACE_CONFIRMATION_FORM: 'workspaceConfirmationForm', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM_DRAFT: 'workspaceCategoryDescriptionHintFormDraft', @@ -722,6 +724,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM]: FormTypes.AddPaymentCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm; + [ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM]: WorkspaceConfirmationForm; [ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; [ONYXKEYS.FORMS.WORKSPACE_COMPANY_CARD_FEED_NAME]: FormTypes.WorkspaceCompanyCardFeedName; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2e895537eaac..bd114bf2fb09 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1335,6 +1335,7 @@ const ROUTES = { }, WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', EXPLANATION_MODAL_ROOT: 'onboarding/explanation', + WORKSPACE_CONFIRMATION: 'workspace/confirmation', TRANSACTION_RECEIPT: { route: 'r/:reportID/transaction/:transactionID/receipt', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index feded7c81a47..7d9b8f20d4d5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -150,6 +150,7 @@ const SCREENS = { DETAILS: 'Details', PROFILE: 'Profile', REPORT_DETAILS: 'Report_Details', + WORKSPACE_CONFIRMATION: 'Workspace_Confirmation', REPORT_SETTINGS: 'Report_Settings', REPORT_DESCRIPTION: 'Report_Description', PARTICIPANTS: 'Participants', @@ -310,6 +311,8 @@ const SCREENS = { EXPORT: 'Report_Details_Export', }, + WORKSPACE_CONFIRMATION: {ROOT: 'Workspace_Confirmation_Root'}, + WORKSPACE: { ACCOUNTING: { ROOT: 'Policy_Accounting', diff --git a/src/components/CurrencyPicker.tsx b/src/components/CurrencyPicker.tsx new file mode 100644 index 000000000000..eae2633425b1 --- /dev/null +++ b/src/components/CurrencyPicker.tsx @@ -0,0 +1,84 @@ +import React, {forwardRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import {View} from 'react-native'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import CurrencySelectionListWithOnyx from './CurrencySelectionList'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import MenuItemWithTopDescription from './MenuItemWithTopDescription'; +import Modal from './Modal'; +import ScreenWrapper from './ScreenWrapper'; +import type {ValuePickerItem, ValuePickerProps} from './ValuePicker/types'; + +function CurrencyPicker({selectedCurrency, label, errorText = '', value, onInputChange, furtherDetails}: ValuePickerProps, forwardedRef: ForwardedRef) { + const StyleUtils = useStyleUtils(); + const styles = useThemeStyles(); + const [isPickerVisible, setIsPickerVisible] = useState(false); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const updateInput = (item: ValuePickerItem) => { + if (item.value !== selectedCurrency) { + onInputChange?.(item.value); + } + hidePickerModal(); + }; + + const descStyle = !selectedCurrency || selectedCurrency.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null; + + return ( + + + + hidePickerModal} + onModalHide={hidePickerModal} + hideModalContentWhileAnimating + useNativeDriver + onBackdropPress={hidePickerModal} + > + + + updateInput({value: item.currencyCode})} + searchInputLabel="Currency" + initiallySelectedCurrencyCode={selectedCurrency} + /> + + + + ); +} + +CurrencyPicker.displayName = 'CurrencyPicker'; + +export default forwardRef(CurrencyPicker); diff --git a/src/components/ValuePicker/types.ts b/src/components/ValuePicker/types.ts index b57c9d32061a..3f1eb101f879 100644 --- a/src/components/ValuePicker/types.ts +++ b/src/components/ValuePicker/types.ts @@ -60,6 +60,8 @@ type ValuePickerProps = { /** Whether to show the tooltip text */ shouldShowTooltips?: boolean; + + selectedCurrency?: string; }; export type {ValuePickerItem, ValueSelectorModalProps, ValuePickerProps, ValuePickerListItem}; diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index f9ac681cb468..dfa6bae4412a 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Currency} from '@src/types/onyx'; import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener'; import * as NumberFormatUtils from './NumberFormatUtils'; @@ -30,6 +31,11 @@ function getCurrencyDecimals(currency: string = CONST.CURRENCY.USD): number { return decimals ?? 2; } +function getCurrency(currency: string = CONST.CURRENCY.USD): Currency | null { + const currencyItem = currencyList?.[currency]; + return currencyItem; +} + /** * Returns the currency's minor unit quantity * e.g. Cent in USD @@ -211,4 +217,5 @@ export { convertToDisplayStringWithoutCurrency, isValidCurrencyCode, convertToShortDisplayString, + getCurrency, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8a64424c8f7d..83786661cbfe 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -32,6 +32,7 @@ import type { TransactionDuplicateNavigatorParamList, TravelNavigatorParamList, WalletStatementNavigatorParamList, + WorkspaceConfirmationNavigatorParamList, } from '@navigation/types'; import type {ThemeStyles} from '@styles/index'; import type {Screen} from '@src/SCREENS'; @@ -121,6 +122,10 @@ const ReportDetailsModalStackNavigator = createModalStackNavigator require('../../../../pages/home/report/ReportDetailsExportPage').default, }); +const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator({ + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: () => require('../../../../pages/workspace/WorkspaceConfirmationPage').default, +}); + const ReportSettingsModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../../pages/settings/Report/ReportSettingsPage').default, [SCREENS.REPORT_SETTINGS.NAME]: () => require('../../../../pages/settings/Report/NamePage').default, @@ -700,4 +705,5 @@ export { SearchSavedSearchModalStackNavigator, MissingPersonalDetailsModalStackNavigator, DebugModalStackNavigator, + WorkspaceConfirmationModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index da1ce32bf747..a75b8a712b5e 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -79,6 +79,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.NEW_CHAT} component={ModalStackNavigators.NewChatModalStackNavigator} /> + + + ['config'] = { path: ROUTES.KEYBOARD_SHORTCUTS, }, [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route, + // [SCREENS.WORKSPACE.CONFIRMATION]: {path: ROUTES.WORKSPACE_CONFIRMATION}, [SCREENS.SETTINGS.SHARE_CODE]: { path: ROUTES.SETTINGS_SHARE_CODE, }, @@ -947,6 +948,11 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_DETAILS.EXPORT]: ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.route, }, }, + [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: { + screens: { + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION, + }, + }, [SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: { screens: { [SCREENS.REPORT_SETTINGS.ROOT]: { @@ -1093,6 +1099,7 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_DESCRIPTION_ROOT]: ROUTES.REPORT_DESCRIPTION.route, }, }, + [SCREENS.RIGHT_MODAL.NEW_CHAT]: { screens: { [SCREENS.NEW_CHAT.ROOT]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3de07f2c801f..59e677e8575c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -901,6 +901,10 @@ type NewChatNavigatorParamList = { [SCREENS.NEW_CHAT.NEW_CHAT_EDIT_NAME]: undefined; }; +type WorkspaceConfirmationNavigatorParamList = { + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: undefined; +}; + type DetailsNavigatorParamList = { [SCREENS.DETAILS_ROOT]: { login: string; @@ -1344,6 +1348,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.EXPENSIFY_CARD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.DOMAIN_CARD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PARTICIPANTS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.ROOM_MEMBERS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams; @@ -1701,4 +1706,5 @@ export type { RestrictedActionParamList, MissingPersonalDetailsParamList, DebugParamList, + WorkspaceConfirmationNavigatorParamList, }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 45203c1db5b6..a619420d92c5 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,6 +4,7 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { + return true return !!betas?.includes(CONST.BETAS.ALL); } diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index f2ceef9069fa..6cbf6d5b0d9e 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -435,6 +435,7 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { + return false; const currentDate = new Date(); const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 5a594a19e15a..903d0bf08b17 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -366,10 +366,12 @@ function endSignOnTransition() { * @param [transitionFromOldDot] Optional, if the user is transitioning from old dot * @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy * @param [backTo] An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. + * @param [policyID] Optional, Policy id. + * @param [file],file */ -function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false, makeMeAdmin = false, backTo = '') { - const policyID = Policy.generatePolicyID(); - Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID, makeMeAdmin); +function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false, makeMeAdmin = false, backTo = '', policyID = '', file?: File) { + const genereatedPolicyID = Policy.generatePolicyID(); + Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID || genereatedPolicyID, makeMeAdmin, file); Navigation.isNavigationReady() .then(() => { @@ -377,7 +379,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po // We must call goBack() to remove the /transition route from history Navigation.goBack(); } - savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin); + savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin, file); Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, backTo)); }) .then(endSignOnTransition); @@ -391,8 +393,8 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po * @param [policyOwnerEmail] Optional, the email of the account to make the owner of the policy * @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy */ -function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false) { - Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID); +function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, file?: File) { + Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', file); } /** diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index b419431bbbb3..94fe15f19e16 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1538,7 +1538,7 @@ function buildOptimisticCustomUnits(reportCurrency?: string): OptimisticCustomUn * @param [policyID] custom policy id we will use for created workspace * @param [makeMeAdmin] leave the calling account as an admin on the policy */ -function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false) { +function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false, file?: File) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, outputCurrency} = buildOptimisticCustomUnits(); @@ -1561,6 +1561,8 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol makeMeAdmin, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + avatarURL: file?.uri ?? null, + originalFileName: file?.name, employeeList: { [sessionEmail]: { role: CONST.POLICY.ROLE.ADMIN, @@ -1592,7 +1594,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol * @param [policyID] custom policy id we will use for created workspace * @param [expenseReportId] the reportID of the expense report that is being used to create the workspace */ -function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: string) { +function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: string, file?: File) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(); @@ -1653,6 +1655,8 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName address: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, + avatarURL: file?.uri, + originalFileName: file?.name, }, }, { @@ -1824,8 +1828,8 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName * @param [policyID] custom policy id we will use for created workspace * @param [engagementChoice] Purpose of using application selected by user in guided setup flow */ -function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), engagementChoice = ''): CreateWorkspaceParams { - const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice); +function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), engagementChoice = '', file?: File): CreateWorkspaceParams { + const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, file); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); return params; diff --git a/src/pages/workspace/WorkspaceConfirmationPage.tsx b/src/pages/workspace/WorkspaceConfirmationPage.tsx new file mode 100644 index 000000000000..054a62b15916 --- /dev/null +++ b/src/pages/workspace/WorkspaceConfirmationPage.tsx @@ -0,0 +1,169 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Avatar from '@components/Avatar'; +import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; +import CurrencyPicker from '@components/CurrencyPicker'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; +import {getCurrency} from '@libs/CurrencyUtils'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import getCurrentUrl from '@libs/Navigation/currentUrl'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import * as App from '@userActions/App'; +import * as Policy from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceConfirmationForm'; +import withPolicy from './withPolicy'; + +function WorkspaceNamePage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + const name = values.name.trim(); + + if (!ValidationUtils.isRequiredFulfilled(name)) { + errors.name = translate('workspace.editor.nameIsRequiredError'); + } else if ([...name].length > CONST.TITLE_CHARACTER_LIMIT) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 + // code units. + ErrorUtils.addErrorMessage(errors, 'name', translate('common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT})); + } + + return errors; + }, + [translate], + ); + + const currentUrl = getCurrentUrl(); + const policyID = Policy.generatePolicyID(); + const [session] = useOnyx(ONYXKEYS.SESSION); + const url = new URL(currentUrl); + // Approved Accountants and Guides can enter a flow where they make a workspace for other users, + // and those are passed as a search parameter when using transition links + const policyOwnerEmail = url.searchParams.get('ownerEmail') ?? ''; + const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const defaultWorkspaceName = Policy.generateDefaultWorkspaceName(policyOwnerEmail); + + const userCurrency = allPersonalDetails?.[session?.accountID ?? 0]?.localCurrencyCode ?? CONST.CURRENCY.USD; + const [currencyCode, setCurrencyCode] = useState(userCurrency); + + const currency = getCurrency(currencyCode); + const [workspaceAvatar, setWorkspaceAvatar] = useState({avatarUri: null, avatarFileName: null, avatarFileType: null}); + const [avatarFile, setAvatarFile] = useState(); + + const stashedLocalAvatarImage = workspaceAvatar?.avatarUri; + + const DefaultAvatar = useCallback( + () => ( + + ), + [workspaceAvatar?.avatarUri, defaultWorkspaceName, styles.alignSelfCenter, styles.avatarXLarge, policyID], + ); + + return ( + + Navigation.goBack()} + /> + + + {translate('workspace.emptyWorkspace.subtitle')} + + { + setAvatarFile(image); + // setWorkspaceAvatar({avatarUri: image.uri ?? '', avatarFileName: image.name ?? '', avatarFileType: image.type}); + }} + onImageRemoved={() => { + setAvatarFile(undefined); + // setWorkspaceAvatar({avatarUri: null, avatarFileName: null, avatarFileType: null}); + }} + size={CONST.AVATAR_SIZE.XLARGE} + avatarStyle={[styles.avatarXLarge, styles.alignSelfCenter]} + shouldDisableViewPhoto + editIcon={Expensicons.Camera} + editIconStyle={styles.smallEditIconAccount} + shouldUseStyleUtilityForAnchorPosition + // style={styles.w100} + type={CONST.ICON_TYPE_WORKSPACE} + style={[styles.w100, styles.alignItemsCenter, styles.mv4, styles.mb6, styles.alignSelfCenter]} + DefaultAvatar={DefaultAvatar} + /> + { + App.createWorkspaceWithPolicyDraftAndNavigateToIt('', val[INPUT_IDS.NAME], false, false, '', policyID, avatarFile as File); + }} + enabledWhenOffline + > + + + + + { + setCurrencyCode(val as string); + }} + /> + + + + + ); +} + +WorkspaceNamePage.displayName = 'WorkspaceNamePage'; + +export default withPolicy(WorkspaceNamePage); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 82503134b09e..9a55ebac2a49 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -396,7 +396,12 @@ function WorkspacesListPage() { subtitle={translate('workspace.emptyWorkspace.subtitle')} ctaText={translate('workspace.new.newWorkspace')} ctaAccessibilityLabel={translate('workspace.new.newWorkspace')} - onCtaPress={() => interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt())} + onCtaPress={() => + interceptAnonymousUser( + () => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION), + // App.createWorkspaceWithPolicyDraftAndNavigateToIt() + ) + } illustration={LottieAnimations.WorkspacePlanet} // We use this style to vertically center the illustration, as the original illustration is not centered illustrationStyle={styles.emptyWorkspaceIllustrationStyle} diff --git a/src/types/form/WorkspaceConfirmationForm.ts b/src/types/form/WorkspaceConfirmationForm.ts new file mode 100644 index 000000000000..8ae2261d018a --- /dev/null +++ b/src/types/form/WorkspaceConfirmationForm.ts @@ -0,0 +1,20 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + NAME: 'name', + CURRENCY: 'currency', +} as const; + +type InputID = ValueOf; + +type WorkspaceConfirmationForm = Form< + InputID, + { + [INPUT_IDS.NAME]: string; + [INPUT_IDS.CURRENCY]: string; + } +>; + +export type {WorkspaceConfirmationForm}; +export default INPUT_IDS; From 96a77e8df1b3905be8f1c3985dc9583e23a74442 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 30 Oct 2024 10:23:52 +0530 Subject: [PATCH 036/533] minor updates. Signed-off-by: krishna2323 --- src/libs/actions/App.ts | 19 ++++++++--- src/libs/actions/Policy/Policy.ts | 33 ++++++++++++++----- .../workspace/WorkspaceConfirmationPage.tsx | 10 +++--- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 903d0bf08b17..4329386406df 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -369,9 +369,18 @@ function endSignOnTransition() { * @param [policyID] Optional, Policy id. * @param [file],file */ -function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false, makeMeAdmin = false, backTo = '', policyID = '', file?: File) { +function createWorkspaceWithPolicyDraftAndNavigateToIt( + policyOwnerEmail = '', + policyName = '', + transitionFromOldDot = false, + makeMeAdmin = false, + backTo = '', + policyID = '', + currency?: string, + file?: File, +) { const genereatedPolicyID = Policy.generatePolicyID(); - Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID || genereatedPolicyID, makeMeAdmin, file); + Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID || genereatedPolicyID, makeMeAdmin, currency, file); Navigation.isNavigationReady() .then(() => { @@ -379,7 +388,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po // We must call goBack() to remove the /transition route from history Navigation.goBack(); } - savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin, file); + savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail, makeMeAdmin, currency, file); Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, backTo)); }) .then(endSignOnTransition); @@ -393,8 +402,8 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', po * @param [policyOwnerEmail] Optional, the email of the account to make the owner of the policy * @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy */ -function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, file?: File) { - Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', file); +function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency?: '', file?: File) { + Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', currency, file); } /** diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 94fe15f19e16..c757cccab100 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1538,9 +1538,9 @@ function buildOptimisticCustomUnits(reportCurrency?: string): OptimisticCustomUn * @param [policyID] custom policy id we will use for created workspace * @param [makeMeAdmin] leave the calling account as an admin on the policy */ -function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false, file?: File) { +function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false, currency = '', file?: File) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const {customUnits, outputCurrency} = buildOptimisticCustomUnits(); + const {customUnits, outputCurrency} = buildOptimisticCustomUnits(currency); const optimisticData: OnyxUpdate[] = [ { @@ -1555,7 +1555,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol ownerAccountID: sessionAccountID, isPolicyExpenseChatEnabled: true, areCategoriesEnabled: true, - outputCurrency, + outputCurrency: currency || outputCurrency, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, customUnits, makeMeAdmin, @@ -1594,10 +1594,19 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol * @param [policyID] custom policy id we will use for created workspace * @param [expenseReportId] the reportID of the expense report that is being used to create the workspace */ -function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), expenseReportId?: string, engagementChoice?: string, file?: File) { +function buildPolicyData( + policyOwnerEmail = '', + makeMeAdmin = false, + policyName = '', + policyID = generatePolicyID(), + expenseReportId?: string, + engagementChoice?: string, + currency?: '', + file?: File, +) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(); + const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(currency); const { adminsChatReportID, @@ -1624,7 +1633,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName owner: sessionEmail, ownerAccountID: sessionAccountID, isPolicyExpenseChatEnabled: true, - outputCurrency, + outputCurrency: currency || outputCurrency, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, autoReporting: true, autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, @@ -1828,8 +1837,16 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName * @param [policyID] custom policy id we will use for created workspace * @param [engagementChoice] Purpose of using application selected by user in guided setup flow */ -function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID(), engagementChoice = '', file?: File): CreateWorkspaceParams { - const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, file); +function createWorkspace( + policyOwnerEmail = '', + makeMeAdmin = false, + policyName = '', + policyID = generatePolicyID(), + engagementChoice = '', + currency?: '', + file?: File, +): CreateWorkspaceParams { + const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, currency, file); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); return params; diff --git a/src/pages/workspace/WorkspaceConfirmationPage.tsx b/src/pages/workspace/WorkspaceConfirmationPage.tsx index 054a62b15916..b105998ba22a 100644 --- a/src/pages/workspace/WorkspaceConfirmationPage.tsx +++ b/src/pages/workspace/WorkspaceConfirmationPage.tsx @@ -28,7 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WorkspaceConfirmationForm'; import withPolicy from './withPolicy'; -function WorkspaceNamePage() { +function WorkspaceConfirmationPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -90,7 +90,7 @@ function WorkspaceNamePage() { { - App.createWorkspaceWithPolicyDraftAndNavigateToIt('', val[INPUT_IDS.NAME], false, false, '', policyID, avatarFile as File); + App.createWorkspaceWithPolicyDraftAndNavigateToIt('', val[INPUT_IDS.NAME], false, false, '', policyID, currencyCode, avatarFile as File); }} enabledWhenOffline > @@ -164,6 +164,6 @@ function WorkspaceNamePage() { ); } -WorkspaceNamePage.displayName = 'WorkspaceNamePage'; +WorkspaceConfirmationPage.displayName = 'WorkspaceConfirmationPage'; -export default withPolicy(WorkspaceNamePage); +export default withPolicy(WorkspaceConfirmationPage); From 879bd6d62510b1a13f05f465752db59767cb70e8 Mon Sep 17 00:00:00 2001 From: chiragsalian Date: Wed, 30 Oct 2024 12:40:16 -0700 Subject: [PATCH 037/533] Updating billing page --- .../Billing-page-coming-soon.md | 49 ------------------- .../billing-and-subscriptions/Billing-page.md | 47 +++++++++++++++++- 2 files changed, 45 insertions(+), 51 deletions(-) delete mode 100644 docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md deleted file mode 100644 index 63c44247658e..000000000000 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Billing and Subscriptions -description: Billing Page Overview ---- - -# Billing Overview - -At the beginning of each month, the Billing Owner for the workspace will be billed for the previous month’s activity. -Your Expensify bill is determined by: -- The number of active members in your workspace -- Whether you have a Collect or Control plan -- Whether you’re on pay-per-use or an annual subscription -- Whether you’re using the Expensify Visa® Commercial Card -- Active members - -An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automatic actions by Concierge. - -## Collect vs Control plan - -Control plan coming soon, comparison to follow. - -## Pay-per-use vs annual subscription - -**Pay-per-use** - -With the pay-per-use rate, you pay the full rate per active member. -- **Collect plan:** $20 per active member -- **Control plan:** $36 per active member - -**Annual** - -With the annual rate, you set your monthly active member count at the beginning of your subscription and get 50% off your monthly active member cost. -- **Collect plan:** $10 per active member -- **Control plan:** $18 per active member - -If you have any additional active members above the number included in your set member count, they will be billed at the pay-per-use rate. You can also choose to increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. For questions, contact Concierge or your account manager. - -## The Expensify Card - -Bundling the Expensify Card with an annual subscription provides you with the lowest monthly price for Expensify. And the more you spend with the Expensify Cards, the lower your bill will be. - -If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per active member when paired with the annual subscription. -- **Collect plan:** $5 per active member -- **Control plan:** $9 per active member - -Additionally, you receive cash back every month that is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. -- 1% cash back on all Expensify Card purchases -- 2% cash back if the amount spent across your Expensify Cards is $250k or more (for U.S. purchases only) - diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md index f945840d65da..63c44247658e 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Billing-page.md @@ -1,6 +1,49 @@ --- title: Billing and Subscriptions -description: An overview of how billing works in Expensify. +description: Billing Page Overview --- -# Coming Soon +# Billing Overview + +At the beginning of each month, the Billing Owner for the workspace will be billed for the previous month’s activity. +Your Expensify bill is determined by: +- The number of active members in your workspace +- Whether you have a Collect or Control plan +- Whether you’re on pay-per-use or an annual subscription +- Whether you’re using the Expensify Visa® Commercial Card +- Active members + +An active member is anyone who chats, creates, submits, approves, reimburses, or exports a report in Expensify in any given month. This includes Copilots and automatic actions by Concierge. + +## Collect vs Control plan + +Control plan coming soon, comparison to follow. + +## Pay-per-use vs annual subscription + +**Pay-per-use** + +With the pay-per-use rate, you pay the full rate per active member. +- **Collect plan:** $20 per active member +- **Control plan:** $36 per active member + +**Annual** + +With the annual rate, you set your monthly active member count at the beginning of your subscription and get 50% off your monthly active member cost. +- **Collect plan:** $10 per active member +- **Control plan:** $18 per active member + +If you have any additional active members above the number included in your set member count, they will be billed at the pay-per-use rate. You can also choose to increase your annual subscription size, which will extend your annual subscription length. However, you cannot reduce your annual subscription size until your current subscription has ended. For questions, contact Concierge or your account manager. + +## The Expensify Card + +Bundling the Expensify Card with an annual subscription provides you with the lowest monthly price for Expensify. And the more you spend with the Expensify Cards, the lower your bill will be. + +If at least 50% of your approved USD spend in a given month is on your company’s Expensify Cards, you will receive an additional 50% discount on the price per active member when paired with the annual subscription. +- **Collect plan:** $5 per active member +- **Control plan:** $9 per active member + +Additionally, you receive cash back every month that is first applied to your Expensify bill, further reducing your price per member. Any leftover cash back is deposited directly into your connected bank account. +- 1% cash back on all Expensify Card purchases +- 2% cash back if the amount spent across your Expensify Cards is $250k or more (for U.S. purchases only) + From a166ca1337970232ccec0290e0b5fb5207d1393f Mon Sep 17 00:00:00 2001 From: Daniel Gale-Rosen Date: Thu, 31 Oct 2024 12:21:03 -0400 Subject: [PATCH 038/533] update param to delegateEmail --- src/libs/API/parameters/AddDelegateParams.ts | 2 +- src/libs/API/parameters/RemoveDelegateParams.ts | 2 +- src/libs/API/parameters/UpdateDelegateRoleParams.ts | 2 +- src/libs/actions/Delegate.ts | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libs/API/parameters/AddDelegateParams.ts b/src/libs/API/parameters/AddDelegateParams.ts index 3f5fcb2a9082..8f30b7ec02b0 100644 --- a/src/libs/API/parameters/AddDelegateParams.ts +++ b/src/libs/API/parameters/AddDelegateParams.ts @@ -1,7 +1,7 @@ import type {DelegateRole} from '@src/types/onyx/Account'; type AddDelegateParams = { - delegate: string; + delegateEmail: string; role: DelegateRole; validateCode: string; }; diff --git a/src/libs/API/parameters/RemoveDelegateParams.ts b/src/libs/API/parameters/RemoveDelegateParams.ts index e19b7680a9b8..59c5fcf9085d 100644 --- a/src/libs/API/parameters/RemoveDelegateParams.ts +++ b/src/libs/API/parameters/RemoveDelegateParams.ts @@ -1,5 +1,5 @@ type RemoveDelegateParams = { - delegate: string; + delegateEmail: string; }; export default RemoveDelegateParams; diff --git a/src/libs/API/parameters/UpdateDelegateRoleParams.ts b/src/libs/API/parameters/UpdateDelegateRoleParams.ts index c7834c0e2d36..ed254b4ebef7 100644 --- a/src/libs/API/parameters/UpdateDelegateRoleParams.ts +++ b/src/libs/API/parameters/UpdateDelegateRoleParams.ts @@ -1,7 +1,7 @@ import type {DelegateRole} from '@src/types/onyx/Account'; type UpdateDelegateRoleParams = { - delegate: string; + delegateEmail: string; role: DelegateRole; validateCode: string; }; diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts index 28f2019bb231..5d7805b2aa9c 100644 --- a/src/libs/actions/Delegate.ts +++ b/src/libs/actions/Delegate.ts @@ -1,7 +1,7 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; -import type {AddDelegateParams, RemoveDelegateParams} from '@libs/API/parameters'; +import type {AddDelegateParams, RemoveDelegateParams, UpdateDelegateRoleParams} from '@libs/API/parameters'; import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import Log from '@libs/Log'; @@ -312,7 +312,7 @@ function addDelegate(email: string, role: DelegateRole, validateCode: string) { }, ]; - const parameters: AddDelegateParams = {delegate: email, validateCode, role}; + const parameters: AddDelegateParams = {delegateEmail: email, validateCode, role}; API.write(WRITE_COMMANDS.ADD_DELEGATE, parameters, {optimisticData, successData, failureData}); } @@ -378,7 +378,7 @@ function removeDelegate(email: string) { }, ]; - const parameters: RemoveDelegateParams = {delegate: email}; + const parameters: RemoveDelegateParams = {delegateEmail: email}; API.write(WRITE_COMMANDS.REMOVE_DELEGATE, parameters, {optimisticData, successData, failureData}); } @@ -486,7 +486,7 @@ function updateDelegateRole(email: string, role: DelegateRole, validateCode: str }, ]; - const parameters = {delegate: email, validateCode, role}; + const parameters: UpdateDelegateRoleParams = {delegateEmail: email, validateCode, role}; API.write(WRITE_COMMANDS.UPDATE_DELEGATE_ROLE, parameters, {optimisticData, successData, failureData}); } From d8076c19009b08cfaf29cdeae29349bd873d9a6f Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 1 Nov 2024 23:00:47 +0300 Subject: [PATCH 039/533] applied small delay to setSelection --- src/components/Composer/implementation/index.native.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index 9f136effa857..e5c2b309ad82 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -65,8 +65,13 @@ function Composer( } // We are setting selection twice to trigger a scroll to the cursor on toggling to smaller composer size. - textInput.current?.setSelection((selection.start || 1) - 1, selection.start); - textInput.current?.setSelection(selection.start, selection.start); + const timeoutID = setTimeout(() => { + textInput.current?.setSelection((selection.start || 1) - 1, selection.start); + textInput.current?.setSelection(selection.start, selection.start); + }, 100); + + return () => clearTimeout(timeoutID); + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isComposerFullSize]); From 295079ffbe9de26548ec2ed84a6e78ccf5082b82 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Fri, 1 Nov 2024 23:02:46 +0300 Subject: [PATCH 040/533] reduced the delay --- src/components/Composer/implementation/index.native.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx index e5c2b309ad82..0eee0be257d4 100644 --- a/src/components/Composer/implementation/index.native.tsx +++ b/src/components/Composer/implementation/index.native.tsx @@ -68,7 +68,7 @@ function Composer( const timeoutID = setTimeout(() => { textInput.current?.setSelection((selection.start || 1) - 1, selection.start); textInput.current?.setSelection(selection.start, selection.start); - }, 100); + }, 0); return () => clearTimeout(timeoutID); From 36a58e35afcaed90cb4889cb3400fd5147e0a3bc Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Sat, 2 Nov 2024 21:33:34 +0700 Subject: [PATCH 041/533] fix gray-out and name issue in offline category update --- src/libs/CategoryUtils.ts | 19 +++++++++++++++++-- src/libs/Middleware/SaveResponseInOnyx.ts | 1 + src/libs/actions/Policy/Category.ts | 15 ++++++++++++++- src/types/onyx/Policy.ts | 1 + 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index 479ae557eab6..7723f98e7345 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -1,7 +1,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import type {Policy, TaxRate, TaxRatesWithDefault} from '@src/types/onyx'; -import type {ApprovalRule, ExpenseRule} from '@src/types/onyx/Policy'; +import type {ApprovalRule, ExpenseRule, MccGroup} from '@src/types/onyx/Policy'; import * as CurrencyUtils from './CurrencyUtils'; function formatDefaultTaxRateText(translate: LocaleContextProps['translate'], taxID: string, taxRate: TaxRate, policyTaxRates?: TaxRatesWithDefault) { @@ -68,4 +68,19 @@ function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: st return categoryDefaultTaxRate; } -export {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApproverRule, getCategoryExpenseRule, getCategoryDefaultTaxRate}; +function updateCategoryInMccGroup(mccGroups: Record, oldCategoryName: string, newCategoryName: string, shouldClearPendingAction?: boolean) { + if (oldCategoryName === newCategoryName) { + return mccGroups; + } + + const updatedGroups: Record = {}; + + for (const [key, group] of Object.entries(mccGroups || {})) { + updatedGroups[key] = + group.category === oldCategoryName ? {...group, category: newCategoryName, pendingAction: shouldClearPendingAction ? null : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE} : group; + } + + return updatedGroups; +} + +export {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApproverRule, getCategoryExpenseRule, getCategoryDefaultTaxRate, updateCategoryInMccGroup}; diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts index 12c1931b0199..677939157e3b 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.ts +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -10,6 +10,7 @@ const requestsToIgnoreLastUpdateID: string[] = [ SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, WRITE_COMMANDS.CLOSE_ACCOUNT, WRITE_COMMANDS.DELETE_MONEY_REQUEST, + WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY, SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES, ]; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 78b0f2dec9e2..5454591b5b5f 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -34,7 +34,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyCategory, RecentlyUsedCategories, Report} from '@src/types/onyx'; -import type {ApprovalRule, CustomUnit, ExpenseRule} from '@src/types/onyx/Policy'; +import type {ApprovalRule, CustomUnit, ExpenseRule, MccGroup} from '@src/types/onyx/Policy'; import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -542,8 +542,12 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string const policyCategoryExpenseRule = CategoryUtils.getCategoryExpenseRule(policy?.rules?.expenseRules ?? [], policyCategory.oldName); const approvalRules = policy?.rules?.approvalRules ?? []; const expenseRules = policy?.rules?.expenseRules ?? []; + const mccGroup = policy?.mccGroup ?? {}; const updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); const updatedExpenseRules: ExpenseRule[] = lodashCloneDeep(expenseRules); + const clonedMccGroup: Record = lodashCloneDeep(mccGroup); + const updatedMccGroup = CategoryUtils.updateCategoryInMccGroup(clonedMccGroup, policyCategory.oldName, policyCategory.newName); + const updatedMccGroupWithClearedPendingAction = CategoryUtils.updateCategoryInMccGroup(clonedMccGroup, policyCategory.oldName, policyCategory.newName, true); if (policyCategoryExpenseRule) { const ruleIndex = updatedExpenseRules.findIndex((rule) => rule.id === policyCategoryExpenseRule.id); @@ -596,10 +600,18 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string approvalRules: updatedApprovalRules, expenseRules: updatedExpenseRules, }, + mccGroup: updatedMccGroup, }, }, ], successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + mccGroup: updatedMccGroupWithClearedPendingAction, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, @@ -639,6 +651,7 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string rules: { approvalRules, }, + mccGroup: updatedMccGroupWithClearedPendingAction, }, }, ], diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index ecc5bd1f6606..39f36f152913 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1901,4 +1901,5 @@ export type { ApprovalRule, ExpenseRule, NetSuiteConnectionConfig, + MccGroup, }; From 9c825da286ac326e7c0bc55d5832dd55d9e89a92 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 4 Nov 2024 10:44:53 +0700 Subject: [PATCH 042/533] Add task specific max length validation --- src/CONST.ts | 11 ++++++ src/components/ExceededCommentLength.tsx | 8 +++-- .../ReportActionCompose.tsx | 34 +++++++++++++++---- src/pages/home/report/ReportFooter.tsx | 13 +------ 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 437ee4e7fd42..b1f4f4f681fa 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -275,6 +275,8 @@ type OnboardingMessageType = { type?: string; }; +const EMAIL_WITH_OPTIONAL_DOMAIN = /(?=((?=[\w'#%+-]+(?:\.[\w'#%+-]+)*@?)[\w.'#%+-]{1,64}(?:@(?:(?=[a-z\d]+(?:-+[a-z\d]+)*\.)(?:[a-z\d-]{1,63}\.)+[a-z]{2,63}))?(?= |_|\b))(?.*))\S{3,254}(?=\k$)/; + const CONST = { HEIC_SIGNATURES: [ '6674797068656963', // 'ftypheic' - Indicates standard HEIC file @@ -2971,6 +2973,15 @@ const CONST = { get EXPENSIFY_POLICY_DOMAIN_NAME() { return new RegExp(`${EXPENSIFY_POLICY_DOMAIN}([a-zA-Z0-9]+)\\${EXPENSIFY_POLICY_DOMAIN_EXTENSION}`); }, + + /** + * Matching task rule by group + * Group 1: Start task rule with [] + * Group 2: Optional email group between \s+....\s* start rule with @+valid email or short mention + * Group 3: Title is remaining characters + */ + // The regex is copied from the expensify-common CONST file, but the domain is optional to accept short mention + TASK_TITLE_WITH_OPTONAL_SHORT_MENTION: `^\\[\\]\\s+(?:@(?:${EMAIL_WITH_OPTIONAL_DOMAIN}))?\\s*([\\s\\S]*)` }, PRONOUNS: { diff --git a/src/components/ExceededCommentLength.tsx b/src/components/ExceededCommentLength.tsx index 2f0887afc8f1..a67558e1b6e0 100644 --- a/src/components/ExceededCommentLength.tsx +++ b/src/components/ExceededCommentLength.tsx @@ -4,7 +4,11 @@ import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import Text from './Text'; -function ExceededCommentLength() { +type ExceededCommentLengthProps = { + shouldUseTitleLimit?: boolean; +}; + +function ExceededCommentLength({shouldUseTitleLimit}: ExceededCommentLengthProps) { const styles = useThemeStyles(); const {numberFormat, translate} = useLocalize(); @@ -13,7 +17,7 @@ function ExceededCommentLength() { style={[styles.textMicro, styles.textDanger, styles.chatItemComposeSecondaryRow, styles.mlAuto, styles.pl2]} numberOfLines={1} > - {translate('composer.commentExceededMaxLength', {formattedMaxLength: numberFormat(CONST.MAX_COMMENT_LENGTH)})} + {translate('composer.commentExceededMaxLength', { formattedMaxLength: numberFormat(shouldUseTitleLimit ? CONST.TITLE_CHARACTER_LIMIT : CONST.MAX_COMMENT_LENGTH) })} ); } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 14908014ca03..024c15051d3b 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -172,6 +172,7 @@ function ReportActionCompose({ * Shows red borders and prevents the comment from being sent */ const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength(); + const [hasExceededTaskTitleLength, setHasExceededTaskTitleLength] = useState(false); const suggestionsRef = useRef(null); const composerRef = useRef(); @@ -332,7 +333,7 @@ function ReportActionCompose({ const hasReportRecipient = !isEmptyObject(reportRecipient); - const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; + const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength || hasExceededTaskTitleLength; // Note: using JS refs is not well supported in reanimated, thus we need to store the function in a shared value // useSharedValue on web doesn't support functions, so we need to wrap it in an object. @@ -393,6 +394,18 @@ function ReportActionCompose({ ], ); + const debouncedValidate = useDebounce((value, reportID) => { + const match = value.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION); + if (!match) { + setHasExceededTaskTitleLength(false); + validateCommentMaxLength(value, { reportID }); + return; + } + + let title = match[3] ? match[3].trim().replace(/\n/g, ' ') : undefined; + setHasExceededTaskTitleLength(title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false); + }, 100); + return ( @@ -425,7 +438,7 @@ function ReportActionCompose({ styles.flexRow, styles.chatItemComposeBox, isComposerFullSize && styles.chatItemFullComposeBox, - hasExceededMaxCommentLength && styles.borderColorDanger, + (hasExceededMaxCommentLength || hasExceededTaskTitleLength) && styles.borderColorDanger, ]} > setIsAttachmentPreviewActive(true)} onModalHide={onAttachmentPreviewClose} - shouldDisableSendButton={hasExceededMaxCommentLength} + shouldDisableSendButton={hasExceededMaxCommentLength || hasExceededTaskTitleLength} > {({displayFileInModal}) => ( <> @@ -453,7 +466,7 @@ function ReportActionCompose({ onAddActionPressed={onAddActionPressed} onItemSelected={onItemSelected} actionButtonRef={actionButtonRef} - shouldDisableAttachmentItem={hasExceededMaxCommentLength} + shouldDisableAttachmentItem={hasExceededMaxCommentLength || hasExceededTaskTitleLength} /> { @@ -494,7 +507,16 @@ function ReportActionCompose({ if (value.length === 0 && isComposerFullSize) { Report.setIsComposerFullSize(reportID, false); } - validateCommentMaxLength(value, {reportID}); + + debouncedValidate(value, reportID); + // const match = value.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION); + // if (!match) { + // validateCommentMaxLength(value, {reportID}); + // return; + // } + + // let title = match[3] ? match[3].trim().replace(/\n/g, ' ') : undefined; + // setHasExceededTaskTitleLength(title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false); }} /> {!shouldUseNarrowLayout && } - {hasExceededMaxCommentLength && } + {(hasExceededMaxCommentLength || hasExceededTaskTitleLength) && } {!isSmallScreenWidth && ( diff --git a/src/pages/home/report/ReportFooter.tsx b/src/pages/home/report/ReportFooter.tsx index c087510374be..ec03393161f3 100644 --- a/src/pages/home/report/ReportFooter.tsx +++ b/src/pages/home/report/ReportFooter.tsx @@ -124,18 +124,7 @@ function ReportFooter({ const handleCreateTask = useCallback( (text: string): boolean => { - /** - * Matching task rule by group - * Group 1: Start task rule with [] - * Group 2: Optional email group between \s+....\s* start rule with @+valid email or short mention - * Group 3: Title is remaining characters - */ - // The regex is copied from the expensify-common CONST file, but the domain is optional to accept short mention - const emailWithOptionalDomainRegex = - /(?=((?=[\w'#%+-]+(?:\.[\w'#%+-]+)*@?)[\w.'#%+-]{1,64}(?:@(?:(?=[a-z\d]+(?:-+[a-z\d]+)*\.)(?:[a-z\d-]{1,63}\.)+[a-z]{2,63}))?(?= |_|\b))(?.*))\S{3,254}(?=\k$)/; - const taskRegex = `^\\[\\]\\s+(?:@(?:${emailWithOptionalDomainRegex.source}))?\\s*([\\s\\S]*)`; - - const match = text.match(taskRegex); + const match = text.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION); if (!match) { return false; } From bd3a70e15a53618d05c1dc6ba28d571ec710ad10 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 4 Nov 2024 10:48:24 +0700 Subject: [PATCH 043/533] remove unnecessary comment --- .../report/ReportActionCompose/ReportActionCompose.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 024c15051d3b..e745836b1b9e 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -509,14 +509,6 @@ function ReportActionCompose({ } debouncedValidate(value, reportID); - // const match = value.match(CONST.REGEX.TASK_TITLE_WITH_OPTONAL_SHORT_MENTION); - // if (!match) { - // validateCommentMaxLength(value, {reportID}); - // return; - // } - - // let title = match[3] ? match[3].trim().replace(/\n/g, ' ') : undefined; - // setHasExceededTaskTitleLength(title ? title.length > CONST.TITLE_CHARACTER_LIMIT : false); }} /> Date: Mon, 4 Nov 2024 22:52:49 -0600 Subject: [PATCH 044/533] Update and rename Expense-Rules.md to Create-Expense-Rules.md Update article formatting and a few small tweaks, rename article to match standard naming convention --- .../expenses/Create-Expense-Rules.md | 61 +++++++++++++++++++ .../expenses/Expense-Rules.md | 55 ----------------- 2 files changed, 61 insertions(+), 55 deletions(-) create mode 100644 docs/articles/expensify-classic/expenses/Create-Expense-Rules.md delete mode 100644 docs/articles/expensify-classic/expenses/Expense-Rules.md diff --git a/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md b/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md new file mode 100644 index 000000000000..c455be3dd721 --- /dev/null +++ b/docs/articles/expensify-classic/expenses/Create-Expense-Rules.md @@ -0,0 +1,61 @@ +--- +title: Create Expense Rules +description: Automatically categorize, tag, and report expenses based on the merchant's name + +--- +Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant’s name. + +# Create expense rules + +1. Hover over **Settings** and click **Account**. +2. Click **Expense Rules**. +2. Click **New Rule**. +3. Add what the merchant name should contain in order for the rule to be applied. *Note: If you enter just a period, the rule will apply to all expenses regardless of the merchant name. Universal Rules will always take precedence over all other expense rules.* +4. Choose from the following rules: +- **Merchant:** Updates the merchant name (e.g., “Starbucks #238” could be changed to “Starbucks”) +- **Category:** Applies a workspace category to the expense +- **Tag:** Applies a tag to the expense (e.g., a Department or Location) +- **Description:** Adds a description to the description field on the expense +- **Reimbursability:** Determines whether the expense will be marked as reimbursable or non-reimbursable +- **Billable**: Determines whether the expense is billable +- **Add to a report named:** Adds the expense to a report with the name you type into the field. If no report with that name exists, a new report will be created if the "Create report if necessary" checkbox is selected. + +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_01.png){:width="100%"} + +{:start="7"} +7. (Optional) To apply the rule to previously entered expenses, select the **Apply to existing matching expenses** checkbox. You can also click **Preview Matching Expenses** to see if your rule matches the intended expenses. + +# How rules are applied + +In general, your expense rules will be applied in order, from **top to bottom**, (i.e., from the first rule). However, other settings can impact how expense rules are applied. Here is the hierarchy that determines how these are applied: + +1. A Universal Rule will **always** be applied over any other expense category rules. Rules that would otherwise change the expense category will **not** override the Universal Rule. +2. If Scheduled Submit and the setting “Enforce Default Report Title” are enabled on the workspace, this will take precedence over any rules trying to add the expense to a report. +3. If the expense is from a company card that is forced to a workspace with strict rule enforcement, those rules will take precedence over individual expense rules. +4. If you belong to a workspace that is tied to an accounting integration, the configuration settings for this connection may update your expense details upon export, even if the expense rules were successfully applied to the expense. + +# Create an expense rule from changes made to an expense + +If you open an expense and change it, you can then create an expense rule based on those changes by selecting the “Create a rule based on your changes" checkbox. *Note: The expense must be saved, reopened, and edited for this option to appear.* + +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_02.png){:width="100%"} + +# Delete an expense rule + +To delete an expense rule, + +1. Hover over **Settings** and click **Account**. +2. Click **Expense Rules**. +3. Scroll down to the rule you’d like to remove and click the trash can icon. + +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_03.png){:width="100%"} + +{% include faq-begin.md %} + +## How can I use expense rules to vendor match when exporting to an accounting package? + +When exporting non-reimbursable expenses to your connected accounting package, the payee field will list "Credit Card Misc." if the merchant name on the expense in Expensify is not an exact match to a vendor in the accounting package. When an exact match is unavailable, "Credit Card Misc." prevents multiple variations of the same vendor (e.g., Starbucks and Starbucks #1234, as is often seen in credit card statements) from being created in your accounting package. + +For repeated expenses, the best practice is to use Expense Rules, which will automatically update the merchant name without having to do it manually each time. This only works for connections to QuickBooks Online, Desktop, and Xero. Vendor matching cannot be performed in this manner for NetSuite or Sage Intacct due to limitations in the API of the accounting package. + +{% include faq-end.md %} diff --git a/docs/articles/expensify-classic/expenses/Expense-Rules.md b/docs/articles/expensify-classic/expenses/Expense-Rules.md deleted file mode 100644 index 295aa8d00cc9..000000000000 --- a/docs/articles/expensify-classic/expenses/Expense-Rules.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Expense Rules -description: Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant's name. - ---- -# Overview -Expense rules allow you to automatically categorize, tag, and report expenses based on the merchant’s name. - -# How to use Expense Rules -**To create an expense rule, follow these steps:** -1. Navigate to **Settings > Account > Expense Rules** -2. Click on **New Rule** -3. Fill in the required information to set up your rule - -When creating an expense rule, you will be able to apply the following rules to expenses: - -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_01.png){:width="100%"} - -- **Merchant:** Updates the merchant name, e.g., “Starbucks #238” could be changed to “Starbucks” -- **Category:** Applies a workspace category to the expense -- **Tag:** Applies a tag to the expense, e.g., a Department or Location -- **Description:** Adds a description to the description field on the expense -- **Reimbursability:** Determines whether the expense will be marked as reimbursable or non-reimbursable -- **Billable**: Determines whether the expense is billable -- **Add to a report named:** Adds the expense to a report with the name you type into the field. If no report with that name exists, a new report will be created - -## Tips on using Expense Rules -- If you'd like to apply a rule to all expenses (“Universal Rule”) rather than just one merchant, simply enter a period [.] and nothing else into the **“When the merchant name contains:”** field. **Note:** Universal Rules will always take precedence over all other rules for category (more on this below). -- You can apply a rule to previously entered expenses by checking the **Apply to existing matching expenses** checkbox. Click “Preview Matching Expenses” to see if your rule matches the intended expenses. -- You can create expense rules while editing an expense. To do this, simply check the box **“Create a rule based on your changes"** at the time of editing. Note that the expense must be saved, reopened, and edited for this option to appear. - - -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_02.png){:width="100%"} - - -To delete an expense rule, go to **Settings > Account > Expense Rules**, scroll down to the rule you’d like to remove, and then click the trash can icon in the upper right corner of the rule: - -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/ExpensifyHelp_ExpenseRules_03.png){:width="100%"} - -# Deep Dive -In general, your expense rules will be applied in order, from **top to bottom**, i.e., from the first rule. However, other settings can impact how expense rules are applied. Here is the hierarchy that determines how these are applied: -1. A Universal Rule will **always** precede over any other expense category rules. Rules that would otherwise change the expense category will **not** override the Universal Rule. -2. If Scheduled Submit and the setting “Enforce Default Report Title” are enabled on the workspace, this will take precedence over any rules trying to add the expense to a report. -3. If the expense is from a Company Card that is forced to a workspace with strict rule enforcement, those rules will take precedence over individual expense rules. -4. If you belong to a workspace that is tied to an accounting integration, the configuration settings for this connection may update your expense details upon export, even if the expense rules were successfully applied to the expense. - - -{% include faq-begin.md %} -## How can I use Expense Rules to vendor match when exporting to an accounting package? -When exporting non-reimbursable expenses to your connected accounting package, the payee field will list "Credit Card Misc." if the merchant name on the expense in Expensify is not an exact match to a vendor in the accounting package. -When an exact match is unavailable, "Credit Card Misc." prevents multiple variations of the same vendor (e.g., Starbucks and Starbucks #1234, as is often seen in credit card statements) from being created in your accounting package. -For repeated expenses, the best practice is to use Expense Rules, which will automatically update the merchant name without having to do it manually each time. -This only works for connections to QuickBooks Online, Desktop, and Xero. Vendor matching cannot be performed in this manner for NetSuite or Sage Intacct due to limitations in the API of the accounting package. - -{% include faq-end.md %} From 1b8128a02178af9647ffe9967c2f162ba90d9d33 Mon Sep 17 00:00:00 2001 From: Ren Jones <153645623+ren-jones@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:59:01 -0600 Subject: [PATCH 045/533] Update redirects.csv Updating for new article name --- docs/redirects.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 06fd7c1ef502..93238b11f55b 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -591,3 +591,4 @@ https://help.expensify.com/articles/expensify-classic/articles/expensify-classic https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Pay-Bills,https://help.expensify.com/articles/expensify-classic/bank-accounts-and-payments/payments/Create-and-Pay-Bills https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/add-a-payment-card-and-view-your-subscription,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Add-a-payment-card-and-view-your-subscription https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page-coming-soon,https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Billing-page +https://help.expensify.com/articles/expensify-classic/expenses/Expense-Rules,https://help.expensify.com/articles/expensify-classic/expenses/Create-Expense-Rules From e55f5a84c57a9bda3344964197e38cebf4ee4ec6 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 5 Nov 2024 17:44:15 +0300 Subject: [PATCH 046/533] updated EmptySearchView to show generic nothing to show message for empty search results --- src/components/Search/index.tsx | 6 ++++- src/pages/Search/EmptySearchView.tsx | 40 ++++++++++++++++------------ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 9238488361b0..8a54b1d14ed1 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -33,6 +33,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SearchResults from '@src/types/onyx/SearchResults'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {useSearchContext} from './SearchContext'; import type {SearchColumnType, SearchQueryJSON, SearchStatus, SelectedTransactionInfo, SelectedTransactions, SortOrder} from './types'; @@ -307,7 +308,10 @@ function Search({queryJSON, onSearchListScroll, contentContainerStyle}: SearchPr if (shouldShowEmptyState) { return ( - + ); } diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 8e61978c169e..e274c981aa21 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -26,6 +26,7 @@ import type {SearchDataTypes} from '@src/types/onyx/SearchResults'; type EmptySearchViewProps = { type: SearchDataTypes; + noExpensesCreatedYet?: boolean; }; const tripsFeatures: FeatureListItem[] = [ @@ -39,7 +40,7 @@ const tripsFeatures: FeatureListItem[] = [ }, ]; -function EmptySearchView({type}: EmptySearchViewProps) { +function EmptySearchView({type, noExpensesCreatedYet = false}: EmptySearchViewProps) { const theme = useTheme(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -114,21 +115,26 @@ function EmptySearchView({type}: EmptySearchViewProps) { ], }; case CONST.SEARCH.DATA_TYPES.EXPENSE: - return { - headerMedia: LottieAnimations.GenericEmptyState, - headerStyles: [StyleUtils.getBackgroundColorStyle(theme.emptyFolderBG)], - title: translate('search.searchResults.emptyExpenseResults.title'), - subtitle: translate('search.searchResults.emptyExpenseResults.subtitle'), - buttons: [ - {buttonText: translate('emptySearchView.takeATour'), buttonAction: () => Link.openExternalLink(navatticLink)}, - { - buttonText: translate('iou.createExpense'), - buttonAction: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.CREATE, ReportUtils.generateReportID())), - success: true, - }, - ], - headerContentStyles: styles.emptyStateFolderWebStyles, - }; + if (noExpensesCreatedYet) { + return { + headerMedia: LottieAnimations.GenericEmptyState, + headerStyles: [StyleUtils.getBackgroundColorStyle(theme.emptyFolderBG)], + title: translate('search.searchResults.emptyExpenseResults.title'), + subtitle: translate('search.searchResults.emptyExpenseResults.subtitle'), + buttons: [ + {buttonText: translate('emptySearchView.takeATour'), buttonAction: () => Link.openExternalLink(navatticLink)}, + { + buttonText: translate('iou.createExpense'), + buttonAction: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.CREATE, ReportUtils.generateReportID())), + success: true, + }, + ], + headerContentStyles: styles.emptyStateFolderWebStyles, + }; + } + // We want to display the default nothing to show message if the user has expenses created + // but the current search filter result is empty. + // eslint-disable-next-line no-fallthrough case CONST.SEARCH.DATA_TYPES.CHAT: case CONST.SEARCH.DATA_TYPES.INVOICE: default: @@ -140,7 +146,7 @@ function EmptySearchView({type}: EmptySearchViewProps) { headerContentStyles: styles.emptyStateFolderWebStyles, }; } - }, [type, StyleUtils, translate, theme, styles, subtitleComponent, ctaErrorMessage, navatticLink]); + }, [type, StyleUtils, translate, theme, styles, subtitleComponent, ctaErrorMessage, navatticLink, noExpensesCreatedYet]); return ( Date: Wed, 6 Nov 2024 05:47:41 +0700 Subject: [PATCH 047/533] remove unused code --- src/libs/Middleware/SaveResponseInOnyx.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts index 677939157e3b..12c1931b0199 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.ts +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -10,7 +10,6 @@ const requestsToIgnoreLastUpdateID: string[] = [ SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, WRITE_COMMANDS.CLOSE_ACCOUNT, WRITE_COMMANDS.DELETE_MONEY_REQUEST, - WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY, SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES, ]; From 09a7c37af25f5f79a9f0a50f02ae65ed9328c39a Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:55:43 +0700 Subject: [PATCH 048/533] Add RENAME_WORKSPACE_CATEGORY to ignore last update ID for fixing first-time category update case --- src/libs/Middleware/SaveResponseInOnyx.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Middleware/SaveResponseInOnyx.ts b/src/libs/Middleware/SaveResponseInOnyx.ts index 12c1931b0199..677939157e3b 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.ts +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -10,6 +10,7 @@ const requestsToIgnoreLastUpdateID: string[] = [ SIDE_EFFECT_REQUEST_COMMANDS.RECONNECT_APP, WRITE_COMMANDS.CLOSE_ACCOUNT, WRITE_COMMANDS.DELETE_MONEY_REQUEST, + WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY, SIDE_EFFECT_REQUEST_COMMANDS.GET_MISSING_ONYX_MESSAGES, ]; From a047d8a5bf0ec13aef46af8998f2821133963ca4 Mon Sep 17 00:00:00 2001 From: Sachin Chavda Date: Wed, 6 Nov 2024 17:08:00 +0530 Subject: [PATCH 049/533] show DelegateNoAccessModal for restricted delegate actions --- src/CONST.ts | 6 ++ src/components/AddressForm.tsx | 6 +- src/components/DelegateNoAccessModal.tsx | 8 +-- src/components/DelegateNoAccessWrapper.tsx | 71 +++++++++++++++++++ src/components/Form/FormProvider.tsx | 2 +- src/pages/AddressPage.tsx | 21 +++++- src/pages/settings/InitialSettingsPage.tsx | 13 ++++ .../Profile/Contacts/ContactMethodsPage.tsx | 2 - .../Profile/Contacts/NewContactMethodPage.tsx | 7 +- .../PersonalDetails/DateOfBirthPage.tsx | 21 ++++-- .../Profile/PersonalDetails/LegalNamePage.tsx | 25 +++++-- .../PersonalDetails/PhoneNumberPage.tsx | 21 +++++- .../Security/SecuritySettingsPage.tsx | 18 ++++- .../settings/Wallet/WalletPage/WalletPage.tsx | 19 +++++ 14 files changed, 212 insertions(+), 28 deletions(-) create mode 100644 src/components/DelegateNoAccessWrapper.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 8a1e9cfbf67c..5c5745f7cfe6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4500,6 +4500,12 @@ const CONST = { ALL: 'all', SUBMITTER: 'submitter', }, + DELEGATE: { + DENIED_ACCESS_VARIANTS: { + DELEGATE: 'delegate', + SUBMITTER: 'submitter', + }, + }, DELEGATE_ROLE_HELPDOT_ARTICLE_LINK: 'https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/', STRIPE_GBP_AUTH_STATUSES: { SUCCEEDED: 'succeeded', diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index 4470481d2be6..709770617fdc 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -56,6 +56,9 @@ type AddressFormProps = { /** A unique Onyx key identifying the form */ formID: typeof ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM | typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM; + + /** Whether or not validation should be skipped */ + skipValidation?: boolean; }; function AddressForm({ @@ -70,6 +73,7 @@ function AddressForm({ street2 = '', submitButtonText = '', zip = '', + skipValidation = false, }: AddressFormProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -139,7 +143,7 @@ function AddressForm({ void; - delegatorEmail: string; }; -export default function DelegateNoAccessModal({isNoDelegateAccessMenuVisible = false, onClose, delegatorEmail = ''}: DelegateNoAccessModalProps) { +export default function DelegateNoAccessModal({isNoDelegateAccessMenuVisible = false, onClose}: DelegateNoAccessModalProps) { const {translate} = useLocalize(); - const noDelegateAccessPromptStart = translate('delegate.notAllowedMessageStart', {accountOwnerEmail: delegatorEmail}); + const {delegatorEmail} = useDelegateUserDetails(); + const noDelegateAccessPromptStart = translate('delegate.notAllowedMessageStart', {accountOwnerEmail: delegatorEmail ?? ''}); const noDelegateAccessHyperLinked = translate('delegate.notAllowedMessageHyperLinked'); - const delegateNoAccessPrompt = ( {noDelegateAccessPromptStart} diff --git a/src/components/DelegateNoAccessWrapper.tsx b/src/components/DelegateNoAccessWrapper.tsx new file mode 100644 index 000000000000..36e675547d7e --- /dev/null +++ b/src/components/DelegateNoAccessWrapper.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import {OnyxEntry, useOnyx} from 'react-native-onyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import AccountUtils from '@libs/AccountUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {Account} from '@src/types/onyx'; +import callOrReturn from '@src/types/utils/callOrReturn'; +import {FullPageNotFoundViewProps} from './BlockingViews/FullPageNotFoundView'; + +const DENIED_ACCESS_VARIANTS = { + [CONST.DELEGATE.DENIED_ACCESS_VARIANTS.DELEGATE]: (account: OnyxEntry) => isDelegate(account), + [CONST.DELEGATE.DENIED_ACCESS_VARIANTS.SUBMITTER]: (account: OnyxEntry) => isSubmitter(account), +} as const satisfies Record) => boolean>; + +type AccessDeniedVariants = keyof typeof DENIED_ACCESS_VARIANTS; + +type DelegateNoAccessWrapperProps = { + accessDeniedVariants?: AccessDeniedVariants[]; + FullPageNotFoundViewProps?: FullPageNotFoundViewProps; + shouldShowFullScreenFallback?: boolean; + children: (() => React.ReactNode) | React.ReactNode; +}; + +type PageNotFoundFallbackProps = { + shouldShowFullScreenFallback?: boolean; +}; + +function isDelegate(account: OnyxEntry) { + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + return isActingAsDelegate; +} + +function isSubmitter(account: OnyxEntry) { + const isDelegateOnlySubmitter = AccountUtils.isDelegateOnlySubmitter(account); + return isDelegateOnlySubmitter; +} + +function PageNotFoundFallback({shouldShowFullScreenFallback}: PageNotFoundFallbackProps) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + return ( + { + if (shouldShowFullScreenFallback) { + Navigation.dismissModal(); + return; + } + Navigation.goBack(); + }} + shouldShowBackButton={!shouldShowFullScreenFallback ? shouldUseNarrowLayout : undefined} + /> + ); +} + +function DelegateNoAccessWrapper({accessDeniedVariants = [], shouldShowFullScreenFallback, ...props}: DelegateNoAccessWrapperProps) { + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isPageAccessDenied = accessDeniedVariants.reduce((acc, variant) => { + const accessDeniedFunction = DENIED_ACCESS_VARIANTS[variant]; + return acc || accessDeniedFunction(account); + }, false); + if (isPageAccessDenied) { + return ; + } + return callOrReturn(props.children); +} + +export default DelegateNoAccessWrapper; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 1d66953c1070..d1b45e1d9327 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -44,7 +44,7 @@ type FormProviderProps = FormProps}) => ReactNode) | ReactNode; /** Callback to validate the form */ - validate?: (values: FormOnyxValues) => FormInputErrors; + validate?: (values: FormOnyxValues) => FormInputErrors | undefined; /** Should validate function be called when input loose focus */ shouldValidateOnBlur?: boolean; diff --git a/src/pages/AddressPage.tsx b/src/pages/AddressPage.tsx index 88e52409751b..670b582e08a2 100644 --- a/src/pages/AddressPage.tsx +++ b/src/pages/AddressPage.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useEffect, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import {type OnyxEntry, useOnyx} from 'react-native-onyx'; import AddressForm from '@components/AddressForm'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -37,6 +38,17 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true, backTo const [city, setCity] = useState(address?.city); const [zipcode, setZipcode] = useState(address?.zip); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + + // For delegates, modifying legal address is a restricted action. + // So, on pressing submit, skip validation and show delegateNoAccessModal + const skipValidation = isActingAsDelegate; + const handleSubmit = (values: FormOnyxValues) => { + isActingAsDelegate ? setIsNoDelegateAccessMenuVisible(true) : updateAddress(values); + }; + useEffect(() => { if (!address) { return; @@ -91,7 +103,7 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true, backTo ) : ( )} + setIsNoDelegateAccessMenuVisible(false)} + /> ); } diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 8c1d68e0a95b..3256a6dbc500 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -8,6 +8,7 @@ import type {ValueOf} from 'type-fest'; import AccountSwitcher from '@components/AccountSwitcher'; import AccountSwitcherSkeletonView from '@components/AccountSwitcherSkeletonView'; import ConfirmModal from '@components/ConfirmModal'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {InitialURLContext} from '@components/InitialURLContextProvider'; @@ -83,6 +84,10 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const network = useNetwork(); const theme = useTheme(); const styles = useThemeStyles(); @@ -242,6 +247,10 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr } : { action() { + if (isActingAsDelegate) { + setIsNoDelegateAccessMenuVisible(true); + return; + } resetExitSurveyForm(() => Navigation.navigate(ROUTES.SETTINGS_EXIT_SURVEY_REASON)); }, }), @@ -435,6 +444,10 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr onCancel={() => toggleSignoutConfirmModal(false)} /> + setIsNoDelegateAccessMenuVisible(false)} + /> ); } diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx index 92a246949c53..dac36a15cefd 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.tsx @@ -36,7 +36,6 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { const [account] = useOnyx(ONYXKEYS.ACCOUNT); const isActingAsDelegate = !!account?.delegatedAccess?.delegate; const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); - const {delegatorEmail} = useDelegateUserDetails(); // Sort the login names by placing the one corresponding to the default contact method as the first item before displaying the contact methods. // The default contact method is determined by checking against the session email (the current login). @@ -132,7 +131,6 @@ function ContactMethodsPage({route}: ContactMethodsPageProps) { setIsNoDelegateAccessMenuVisible(false)} - delegatorEmail={delegatorEmail ?? ''} /> ); diff --git a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx index c2a7e1b6712c..d23af0371ef3 100644 --- a/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx +++ b/src/pages/settings/Profile/Contacts/NewContactMethodPage.tsx @@ -3,6 +3,7 @@ import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -110,7 +111,8 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { }, [navigateBackTo]); return ( - + // + loginInputRef.current?.focus()} includeSafeAreaPaddingBottom={false} @@ -174,7 +176,8 @@ function NewContactMethodPage({route}: NewContactMethodPageProps) { description={translate('contacts.enterMagicCode', {contactMethod})} /> - + + // ); } diff --git a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx index e91093731c03..882f29d51b99 100644 --- a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx @@ -1,8 +1,9 @@ import {subYears} from 'date-fns'; -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; @@ -16,7 +17,7 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/DateOfBirthForm'; +import INPUT_IDS, {DateOfBirthForm} from '@src/types/form/DateOfBirthForm'; import type {PrivatePersonalDetails} from '@src/types/onyx'; type DateOfBirthPageOnyxProps = { @@ -30,6 +31,9 @@ type DateOfBirthPageProps = DateOfBirthPageOnyxProps; function DateOfBirthPage({privatePersonalDetails, isLoadingApp = true}: DateOfBirthPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); /** * @returns An object containing the errors for each inputID @@ -48,6 +52,13 @@ function DateOfBirthPage({privatePersonalDetails, isLoadingApp = true}: DateOfBi return errors; }, []); + // For delegates, modifying legal DOB is a restricted action. + // So, on pressing submit, skip validation and show delegateNoAccessModal + + const skipValidation = isActingAsDelegate; + const handleSubmit = (DOB: DateOfBirthForm) => { + isActingAsDelegate ? setIsNoDelegateAccessMenuVisible(true) : PersonalDetails.updateDateOfBirth(DOB); + }; return ( diff --git a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx index 99e9c910cbdf..cf9a8ec398a3 100644 --- a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx @@ -1,7 +1,8 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; @@ -33,12 +34,14 @@ type LegalNamePageProps = LegalNamePageOnyxProps; const updateLegalName = (values: PrivatePersonalDetails) => { PersonalDetails.updateLegalName(values.legalFirstName?.trim() ?? '', values.legalLastName?.trim() ?? ''); }; - function LegalNamePage({privatePersonalDetails, isLoadingApp = true}: LegalNamePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const legalFirstName = privatePersonalDetails?.legalFirstName ?? ''; const legalLastName = privatePersonalDetails?.legalLastName ?? ''; + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); const validate = useCallback( (values: FormOnyxValues) => { @@ -83,6 +86,14 @@ function LegalNamePage({privatePersonalDetails, isLoadingApp = true}: LegalNameP [translate], ); + // For delegates, modifying legal Name is a restricted action. + // So, on pressing submit, skip validation and show delegateNoAccessModal + + const skipValidation = isActingAsDelegate; + const handleSubmit = (values: PrivatePersonalDetails) => { + isActingAsDelegate ? setIsNoDelegateAccessMenuVisible(true) : updateLegalName(values); + }; + return ( @@ -130,6 +141,10 @@ function LegalNamePage({privatePersonalDetails, isLoadingApp = true}: LegalNameP )} + setIsNoDelegateAccessMenuVisible(false)} + /> ); } diff --git a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx index 12c6f011c6a2..536fa510ef54 100644 --- a/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/PhoneNumberPage.tsx @@ -1,6 +1,7 @@ import {Str} from 'expensify-common'; -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -32,6 +33,10 @@ function PhoneNumberPage() { const validateLoginError = ErrorUtils.getEarliestErrorField(privatePersonalDetails, 'phoneNumber'); const currenPhoneNumber = privatePersonalDetails?.phoneNumber ?? ''; + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const updatePhoneNumber = (values: PrivatePersonalDetails) => { // Clear the error when the user tries to submit the form if (validateLoginError) { @@ -66,6 +71,12 @@ function PhoneNumberPage() { }, [translate, validateLoginError], ); + // For delegates, modifying Phone Number is a restricted action. + // So, on pressing submit, skip validation and show delegateNoAccessModal + const skipValidation = isActingAsDelegate; + const handleSubmit = (values: PrivatePersonalDetails) => { + isActingAsDelegate ? setIsNoDelegateAccessMenuVisible(true) : updatePhoneNumber(values); + }; return ( @@ -112,6 +123,10 @@ function PhoneNumberPage() { )} + setIsNoDelegateAccessMenuVisible(false)} + /> ); } diff --git a/src/pages/settings/Security/SecuritySettingsPage.tsx b/src/pages/settings/Security/SecuritySettingsPage.tsx index cd8e7c14d882..f646725319b3 100644 --- a/src/pages/settings/Security/SecuritySettingsPage.tsx +++ b/src/pages/settings/Security/SecuritySettingsPage.tsx @@ -5,6 +5,7 @@ import {Dimensions, View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import {FallbackAvatar} from '@components/Icon/Expensicons'; @@ -20,6 +21,7 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useDelegateUserDetails from '@hooks/useDelegateUserDetails'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -63,6 +65,10 @@ function SecuritySettingsPage() { anchorPositionRight: 0, }); + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + + // const [account] = useOnyx(ONYXKEYS.ACCOUNT); + // const isActingAsDelegate = !!account?.delegatedAccess?.delegate; const setMenuPosition = useCallback(() => { if (!delegateButtonRef.current) { return; @@ -92,7 +98,9 @@ function SecuritySettingsPage() { setShouldShowDelegatePopoverMenu(true); setSelectedDelegate(delegate); }; - + const showDelegateNoAccessMenu = () => { + setIsNoDelegateAccessMenuVisible(true); + }; useLayoutEffect(() => { const popoverPositionListener = Dimensions.addEventListener('change', () => { debounce(setMenuPosition, CONST.TIMING.RESIZE_DEBOUNCE_TIME)(); @@ -111,12 +119,12 @@ function SecuritySettingsPage() { { translationKey: 'twoFactorAuth.headerTitle', icon: Expensicons.Shield, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute())), + action: isActingAsDelegate ? showDelegateNoAccessMenu : waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute())), }, { translationKey: 'closeAccountPage.closeAccount', icon: Expensicons.ClosedSign, - action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), + action: isActingAsDelegate ? showDelegateNoAccessMenu : waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CLOSE)), }, ]; @@ -333,6 +341,10 @@ function SecuritySettingsPage() { /> + setIsNoDelegateAccessMenuVisible(false)} + /> )} diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 7b9366370349..79b0a434a5ad 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -7,6 +7,7 @@ import {ActivityIndicator, Dimensions, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import ConfirmModal from '@components/ConfirmModal'; +import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -22,6 +23,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; +import useDelegateUserDetails from '@hooks/useDelegateUserDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePaymentMethodState from '@hooks/usePaymentMethodState'; @@ -59,6 +61,10 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {initialValue: {}}); const [isUserValidated] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.validated}); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); + const isActingAsDelegate = !!account?.delegatedAccess?.delegate; + const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -179,6 +185,10 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { setMenuPosition(); return; } + if (isActingAsDelegate) { + setIsNoDelegateAccessMenuVisible(true); + return; + } setShouldShowAddPaymentMenu(true); setMenuPosition(); }; @@ -499,6 +509,11 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { } onPress={() => { + if (isActingAsDelegate) { + setIsNoDelegateAccessMenuVisible(true); + return; + } + if (!isUserValidated) { Navigation.navigate(ROUTES.SETTINGS_WALLET_VERIFY_ACCOUNT.getRoute(ROUTES.SETTINGS_ENABLE_PAYMENTS)); return; @@ -595,6 +610,10 @@ function WalletPage({shouldListenForResize = false}: WalletPageProps) { anchorRef={addPaymentMethodAnchorRef} shouldShowPersonalBankAccountOption /> + setIsNoDelegateAccessMenuVisible(false)} + /> ); } From f283a185438b2e4addea67acc03ac10bb6bf8055 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 7 Nov 2024 14:15:14 +0530 Subject: [PATCH 050/533] updated ONYXKEYS file. Signed-off-by: krishna2323 --- src/ONYXKEYS.ts | 4 ++-- src/components/CurrencyPicker.tsx | 5 ++++- src/components/ValuePicker/types.ts | 2 -- .../AppNavigator/ModalStackNavigators/index.tsx | 8 ++++---- .../AppNavigator/Navigators/RightModalNavigator.tsx | 11 +++++------ src/libs/Navigation/linkingConfig/config.ts | 12 +++++------- src/libs/Navigation/types.ts | 10 +++++----- src/libs/Permissions.ts | 1 - src/libs/SubscriptionUtils.ts | 1 - src/libs/actions/App.ts | 2 +- src/libs/actions/Policy/Policy.ts | 2 +- src/types/form/index.ts | 1 + 12 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f15337b22e75..441f85b01a16 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -3,7 +3,6 @@ import type CONST from './CONST'; import type {OnboardingCompanySizeType, OnboardingPurposeType} from './CONST'; import type Platform from './libs/getPlatform/types'; import type * as FormTypes from './types/form'; -import type {WorkspaceConfirmationForm} from './types/form/WorkspaceConfirmationForm'; import type * as OnyxTypes from './types/onyx'; import type {Attendee} from './types/onyx/IOU'; import type Onboarding from './types/onyx/Onboarding'; @@ -547,6 +546,7 @@ const ONYXKEYS = { WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', WORKSPACE_CONFIRMATION_FORM: 'workspaceConfirmationForm', + WORKSPACE_CONFIRMATION_FORM_DRAFT: 'workspaceConfirmationFormDraft', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM_DRAFT: 'workspaceCategoryDescriptionHintFormDraft', @@ -730,7 +730,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM]: FormTypes.AddPaymentCardForm; [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm; - [ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM]: WorkspaceConfirmationForm; + [ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM]: FormTypes.WorkspaceConfirmationForm; [ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; [ONYXKEYS.FORMS.WORKSPACE_COMPANY_CARD_FEED_NAME]: FormTypes.WorkspaceCompanyCardFeedName; diff --git a/src/components/CurrencyPicker.tsx b/src/components/CurrencyPicker.tsx index eae2633425b1..7f60eb94fc34 100644 --- a/src/components/CurrencyPicker.tsx +++ b/src/components/CurrencyPicker.tsx @@ -12,7 +12,10 @@ import Modal from './Modal'; import ScreenWrapper from './ScreenWrapper'; import type {ValuePickerItem, ValuePickerProps} from './ValuePicker/types'; -function CurrencyPicker({selectedCurrency, label, errorText = '', value, onInputChange, furtherDetails}: ValuePickerProps, forwardedRef: ForwardedRef) { +type CurrencyPickerProps = { + selectedCurrency?: string; +}; +function CurrencyPicker({selectedCurrency, label, errorText = '', value, onInputChange, furtherDetails}: ValuePickerProps & CurrencyPickerProps, forwardedRef: ForwardedRef) { const StyleUtils = useStyleUtils(); const styles = useThemeStyles(); const [isPickerVisible, setIsPickerVisible] = useState(false); diff --git a/src/components/ValuePicker/types.ts b/src/components/ValuePicker/types.ts index 3f1eb101f879..b57c9d32061a 100644 --- a/src/components/ValuePicker/types.ts +++ b/src/components/ValuePicker/types.ts @@ -60,8 +60,6 @@ type ValuePickerProps = { /** Whether to show the tooltip text */ shouldShowTooltips?: boolean; - - selectedCurrency?: string; }; export type {ValuePickerItem, ValueSelectorModalProps, ValuePickerProps, ValuePickerListItem}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 83786661cbfe..1a91fa55ee91 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -122,10 +122,6 @@ const ReportDetailsModalStackNavigator = createModalStackNavigator require('../../../../pages/home/report/ReportDetailsExportPage').default, }); -const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator({ - [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: () => require('../../../../pages/workspace/WorkspaceConfirmationPage').default, -}); - const ReportSettingsModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../../pages/settings/Report/ReportSettingsPage').default, [SCREENS.REPORT_SETTINGS.NAME]: () => require('../../../../pages/settings/Report/NamePage').default, @@ -134,6 +130,10 @@ const ReportSettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Report/VisibilityPage').default, }); +const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator({ + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: () => require('../../../../pages/workspace/WorkspaceConfirmationPage').default, +}); + const TaskModalStackNavigator = createModalStackNavigator({ [SCREENS.TASK.TITLE]: () => require('../../../../pages/tasks/TaskTitlePage').default, [SCREENS.TASK.ASSIGNEE]: () => require('../../../../pages/tasks/TaskAssigneeSelectorModal').default, diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index a75b8a712b5e..a39a87bbc496 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -79,7 +79,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.NEW_CHAT} component={ModalStackNavigators.NewChatModalStackNavigator} /> - - + - + ['config'] = { path: ROUTES.KEYBOARD_SHORTCUTS, }, [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route, - // [SCREENS.WORKSPACE.CONFIRMATION]: {path: ROUTES.WORKSPACE_CONFIRMATION}, [SCREENS.SETTINGS.SHARE_CODE]: { path: ROUTES.SETTINGS_SHARE_CODE, }, @@ -951,11 +950,6 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_DETAILS.EXPORT]: ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.route, }, }, - [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: { - screens: { - [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION, - }, - }, [SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: { screens: { [SCREENS.REPORT_SETTINGS.ROOT]: { @@ -1102,7 +1096,6 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_DESCRIPTION_ROOT]: ROUTES.REPORT_DESCRIPTION.route, }, }, - [SCREENS.RIGHT_MODAL.NEW_CHAT]: { screens: { [SCREENS.NEW_CHAT.ROOT]: { @@ -1129,6 +1122,11 @@ const config: LinkingOptions['config'] = { }, }, }, + [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: { + screens: { + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION, + }, + }, [SCREENS.RIGHT_MODAL.NEW_TASK]: { screens: { [SCREENS.NEW_TASK.ROOT]: ROUTES.NEW_TASK.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4b5844c764c0..87e2509c7ae2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -904,10 +904,6 @@ type NewChatNavigatorParamList = { [SCREENS.NEW_CHAT.NEW_CHAT_EDIT_NAME]: undefined; }; -type WorkspaceConfirmationNavigatorParamList = { - [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: undefined; -}; - type DetailsNavigatorParamList = { [SCREENS.DETAILS_ROOT]: { login: string; @@ -1178,6 +1174,10 @@ type MoneyRequestNavigatorParamList = { }; }; +type WorkspaceConfirmationNavigatorParamList = { + [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: undefined; +}; + type NewTaskNavigatorParamList = { [SCREENS.NEW_TASK.ROOT]: { backTo?: Routes; @@ -1351,10 +1351,10 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.EXPENSIFY_CARD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.DOMAIN_CARD]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: NavigatorScreenParams; - [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PARTICIPANTS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.ROOM_MEMBERS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.NEW_TASK]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TASK_DETAILS]: NavigatorScreenParams; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 1396c2cd8219..b0591d1ad42b 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -3,7 +3,6 @@ import CONST from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return true return !!betas?.includes(CONST.BETAS.ALL); } diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 6cbf6d5b0d9e..f2ceef9069fa 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -435,7 +435,6 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { - return false; const currentDate = new Date(); const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 4329386406df..7ac7abc278e9 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -402,7 +402,7 @@ function createWorkspaceWithPolicyDraftAndNavigateToIt( * @param [policyOwnerEmail] Optional, the email of the account to make the owner of the policy * @param [makeMeAdmin] Optional, leave the calling account as an admin on the policy */ -function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency?: '', file?: File) { +function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, policyOwnerEmail = '', makeMeAdmin = false, currency = '', file?: File) { Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID, '', currency, file); } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index d5b9713782b1..12bb63e60a71 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1853,7 +1853,7 @@ function createWorkspace( policyName = '', policyID = generatePolicyID(), engagementChoice = '', - currency?: '', + currency = '', file?: File, ): CreateWorkspaceParams { const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice, currency, file); diff --git a/src/types/form/index.ts b/src/types/form/index.ts index ddecc5cd634e..6e29c6b08784 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -85,3 +85,4 @@ export type {WorkspaceCompanyCardFeedName} from './WorkspaceCompanyCardFeedName' export type {SearchSavedSearchRenameForm} from './SearchSavedSearchRenameForm'; export type {WorkspaceCompanyCardEditName} from './WorkspaceCompanyCardEditName'; export type {PersonalDetailsForm} from './PersonalDetailsForm'; +export type {WorkspaceConfirmationForm} from './WorkspaceConfirmationForm'; From d442d2f825b590f64d137ca96adc258d2cfd7e12 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 7 Nov 2024 14:24:35 +0530 Subject: [PATCH 051/533] minor updates. Signed-off-by: krishna2323 --- .../WorkspaceCardCreateAWorkspace.tsx | 5 +++-- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 7 +------ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx b/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx index ecfdb0d83ace..bf250a063582 100644 --- a/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx +++ b/src/pages/WorkspaceSwitcherPage/WorkspaceCardCreateAWorkspace.tsx @@ -4,7 +4,8 @@ import * as Illustrations from '@components/Icon/Illustrations'; import Section, {CARD_LAYOUT} from '@components/Section'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as App from '@userActions/App'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; function WorkspaceCardCreateAWorkspace() { const styles = useThemeStyles(); @@ -21,7 +22,7 @@ function WorkspaceCardCreateAWorkspace() { >