diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml
index 276d0073b132..b86b68cc7d7d 100644
--- a/.github/actions/composite/buildAndroidE2EAPK/action.yml
+++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml
@@ -51,7 +51,7 @@ runs:
- uses: Expensify/App/.github/actions/composite/setupNode@main
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: "oracle"
java-version: "17"
diff --git a/.github/scripts/verifyRedirect.sh b/.github/scripts/verifyRedirect.sh
index 3d96ba17a799..05c402ad7766 100755
--- a/.github/scripts/verifyRedirect.sh
+++ b/.github/scripts/verifyRedirect.sh
@@ -32,6 +32,12 @@ while read -r line; do
SOURCE_URL=${LINE_PARTS[0]}
DEST_URL=${LINE_PARTS[1]}
+ # Make sure that the source and destination are not identical
+ if [[ "$SOURCE_URL" == "$DEST_URL" ]]; then
+ error "Source and destination URLs are identical: $SOURCE_URL"
+ exit 1
+ fi
+
# Make sure the format of the line is as expected.
if [[ "${#LINE_PARTS[@]}" -gt 2 ]]; then
error "Found a line with more than one comma: $line"
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index df69fbe76bf7..bacab79998f9 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -54,7 +54,7 @@ jobs:
uses: ./.github/actions/composite/setupNode
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: '17'
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index c77d122de049..21f7fcedfe85 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -81,7 +81,7 @@ jobs:
uses: ./.github/actions/composite/setupNode
- name: Setup Java
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: '17'
diff --git a/__mocks__/react-native.ts b/__mocks__/react-native.ts
index 3deeabf6df2a..4c2a86818e9b 100644
--- a/__mocks__/react-native.ts
+++ b/__mocks__/react-native.ts
@@ -26,7 +26,6 @@ jest.doMock('react-native', () => {
type ReactNativeMock = typeof ReactNative & {
NativeModules: typeof ReactNative.NativeModules & {
BootSplash: {
- getVisibilityStatus: typeof BootSplash.getVisibilityStatus;
hide: typeof BootSplash.hide;
logoSizeRatio: number;
navigationBarHeight: number;
@@ -46,7 +45,6 @@ jest.doMock('react-native', () => {
NativeModules: {
...ReactNative.NativeModules,
BootSplash: {
- getVisibilityStatus: jest.fn(),
hide: jest.fn(),
logoSizeRatio: 1,
navigationBarHeight: 0,
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 48ee8c8455f7..8b1a23b72e95 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009002901
- versionName "9.0.29-1"
+ versionCode 1009002905
+ versionName "9.0.29-5"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/assets/images/product-illustrations/broken-magnifying-glass.svg b/assets/images/product-illustrations/broken-magnifying-glass.svg
new file mode 100644
index 000000000000..0b85744c1869
--- /dev/null
+++ b/assets/images/product-illustrations/broken-magnifying-glass.svg
@@ -0,0 +1,28 @@
+
diff --git a/docs/articles/expensify-classic/workspaces/Create-tags.md b/docs/articles/expensify-classic/workspaces/Create-tags.md
index 74967ee04c7a..ad3f51bc8c58 100644
--- a/docs/articles/expensify-classic/workspaces/Create-tags.md
+++ b/docs/articles/expensify-classic/workspaces/Create-tags.md
@@ -55,10 +55,10 @@ When you first connect your accounting integration (for example, QuickBooks Onli
You can add a list of single tags by importing them in a .csv, .txt, .xls, or .xlsx spreadsheet.
1. Determine whether you will use independent (a separate tag for department and project) or dependent tags (the project tags populate different options based on the department selected), and whether you will capture general ledge (GL) codes. Then use one of the following templates to build your tags list:
- - [Dependent tags with GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fus.v-cdn.net%2F6030147%2Fuploads%2FO7G7UWJCCFXC%2Fdependant-tag-with-gl-code-template.xlsx)
- - [Dependent tags without GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fus.v-cdn.net%2F6030147%2Fuploads%2FY7DCMUVLSHEO%2Fdependant-tag-without-gl-code-template.xlsx)
- - [Independent tags with GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1618929581886-Independent%2Bwith%2BGL%2Bcodes%2Bformat%2B-%2BSheet1.csv)
- - [Independent tags without GL codes](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fs3-us-west-1.amazonaws.com%2Fconcierge-responses-expensify-com%2Fuploads%252F1618929575401-Independent%2Bwithout%2BGL%2Bcodes%2Bformat%2B-%2BSheet1.csv)
+ - [Dependent tags with GL codes]({{site.url}}/assets/Files/Dependent+with+GL+codes+format.csv)
+ - [Dependent tags without GL codes]({{site.url}}/assets/Files/Dependent+without+GL+codes+format.csv)
+ - [Independent tags with GL codes]({{site.url}}/assets/Files/Independent+with+GL+codes+format.csv)
+ - [Independent tags without GL codes]({{site.url}}/assets/Files/Independent+without+GL+codes+format.csv)
{% include info.html %}
If you have more than 50,000 tags, divide them into two separate files.
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 924e1507a2d1..c01a583354d8 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 9.0.29.1
+ 9.0.29.5
FullStory
OrgId
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 970b43c8c99c..e0afb4af6447 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.29.1
+ 9.0.29.5
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 5ae53d6d75d2..14b7c13c7bd6 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.29
CFBundleVersion
- 9.0.29.1
+ 9.0.29.5
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 4bef19aa97cc..792969184480 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.29-1",
+ "version": "9.0.29-5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.29-1",
+ "version": "9.0.29-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 9e23bf932cf0..c5dd5b52354a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.29-1",
+ "version": "9.0.29-5",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/patches/react-native+0.75.2+017+redactAppParameters.patch b/patches/react-native+0.75.2+017+redactAppParameters.patch
new file mode 100644
index 000000000000..7c4273c6d0c1
--- /dev/null
+++ b/patches/react-native+0.75.2+017+redactAppParameters.patch
@@ -0,0 +1,40 @@
+diff --git a/node_modules/react-native/Libraries/ReactNative/AppRegistry.js b/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
+index 68bd389..be9b5bf 100644
+--- a/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
++++ b/node_modules/react-native/Libraries/ReactNative/AppRegistry.js
+@@ -232,12 +232,34 @@ const AppRegistry = {
+ appParameters: Object,
+ displayMode?: number,
+ ): void {
++ const redactAppParameters = (parameters) => {
++ const initialProps = parameters['initialProps'];
++ const url = initialProps['url'];
++
++ if(!url) {
++ return parameters;
++ }
++
++ const sensitiveParams = ['authToken', 'autoGeneratedPassword', 'autoGeneratedLogin'];
++ const [urlBase, queryString] = url.split('?');
++
++ if (!queryString) {
++ return parameters;
++ }
++
++ const redactedSearchParams = queryString.split('&').map((param) => {
++ const [key, value] = param.split('=');
++ return `${key}=${sensitiveParams.includes(key) ? '' : value}`
++ });
++ return {...parameters, initialProps: {...initialProps, url: `${urlBase}?${redactedSearchParams.join('&')}`}};
++ }
++
+ if (appKey !== 'LogBox') {
+ const msg =
+ 'Updating props for Surface "' +
+ appKey +
+ '" with ' +
+- JSON.stringify(appParameters);
++ JSON.stringify(redactAppParameters(appParameters));
+ infoLog(msg);
+ BugReporting.addSource(
+ 'AppRegistry.setSurfaceProps' + runCount++,
diff --git a/src/App.tsx b/src/App.tsx
index 860ab5420075..cf0fd5528eec 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -37,6 +37,7 @@ import {ReportIDsContextProvider} from './hooks/useReportIDs';
import OnyxUpdateManager from './libs/actions/OnyxUpdateManager';
import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext';
import type {Route} from './ROUTES';
+import {SplashScreenStateContextProvider} from './SplashScreenStateContext';
type AppProps = {
/** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */
@@ -63,46 +64,48 @@ function App({url}: AppProps) {
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/CONST.ts b/src/CONST.ts
index 070b517369f9..11fbaf986663 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1524,6 +1524,7 @@ const CONST = {
},
CUSTOM_SEGMENT_FIELDS: ['segmentName', 'internalID', 'scriptID', 'mapping'],
CUSTOM_LIST_FIELDS: ['listName', 'internalID', 'transactionFieldID', 'mapping'],
+ CUSTOM_FORM_ID_ENABLED: 'enabled',
CUSTOM_FORM_ID_TYPE: {
REIMBURSABLE: 'reimbursable',
NON_REIMBURSABLE: 'nonReimbursable',
@@ -2291,6 +2292,17 @@ const CONST = {
VISA: 'vcf',
AMEX: 'gl1025',
},
+ STEP_NAMES: ['1', '2', '3', '4'],
+ STEP: {
+ ASSIGNEE: 'Assignee',
+ CARD: 'Card',
+ TRANSACTION_START_DATE: 'TransactionStartDate',
+ CONFIRMATION: 'Confirmation',
+ },
+ TRANSACTION_START_DATE_OPTIONS: {
+ FROM_BEGINNING: 'fromBeginning',
+ CUSTOM: 'custom',
+ },
},
EXPENSIFY_CARD: {
BANK: 'Expensify Card',
@@ -5560,6 +5572,12 @@ const CONST = {
},
},
+ BOOT_SPLASH_STATE: {
+ VISIBLE: 'visible',
+ READY_TO_BE_HIDDEN: 'readyToBeHidden',
+ HIDDEN: `hidden`,
+ },
+
CSV_IMPORT_COLUMNS: {
EMAIL: 'email',
NAME: 'name',
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 10389f69a44c..62e7839b21f0 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -1,5 +1,5 @@
import {Audio} from 'expo-av';
-import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
+import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
import type {NativeEventSubscription} from 'react-native';
import {AppState, Linking, NativeModules, Platform} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
@@ -20,8 +20,8 @@ import {updateLastRoute} from './libs/actions/App';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
import * as Report from './libs/actions/Report';
import * as User from './libs/actions/User';
+import {handleHybridAppOnboarding} from './libs/actions/Welcome';
import * as ActiveClientManager from './libs/ActiveClientManager';
-import BootSplash from './libs/BootSplash';
import FS from './libs/Fullstory';
import * as Growl from './libs/Growl';
import Log from './libs/Log';
@@ -42,6 +42,7 @@ import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/Popo
import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu';
import type {Route} from './ROUTES';
import ROUTES from './ROUTES';
+import SplashScreenStateContext from './SplashScreenStateContext';
import type {ScreenShareRequest} from './types/onyx';
Onyx.registerLogger(({level, message}) => {
@@ -80,13 +81,6 @@ type ExpensifyOnyxProps = {
type ExpensifyProps = ExpensifyOnyxProps;
-// HybridApp needs access to SetStateAction in order to properly hide SplashScreen when React Native was booted before.
-type SplashScreenHiddenContextType = {isSplashHidden?: boolean; setIsSplashHidden: React.Dispatch>};
-
-const SplashScreenHiddenContext = React.createContext({
- setIsSplashHidden: () => {},
-});
-
function Expensify({
isCheckingPublicRoom = true,
updateAvailable,
@@ -99,12 +93,13 @@ function Expensify({
const appStateChangeListener = useRef(null);
const [isNavigationReady, setIsNavigationReady] = useState(false);
const [isOnyxMigrated, setIsOnyxMigrated] = useState(false);
- const [isSplashHidden, setIsSplashHidden] = useState(false);
+ const {splashScreenState, setSplashScreenState} = useContext(SplashScreenStateContext);
const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false);
const {translate} = useLocalize();
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [session] = useOnyx(ONYXKEYS.SESSION);
const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
+ const [tryNewDotData] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT);
const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false);
useEffect(() => {
@@ -123,11 +118,21 @@ function Expensify({
setAttemptedToOpenPublicRoom(true);
}, [isCheckingPublicRoom]);
+ useEffect(() => {
+ if (splashScreenState !== CONST.BOOT_SPLASH_STATE.HIDDEN || tryNewDotData === undefined) {
+ return;
+ }
+
+ handleHybridAppOnboarding();
+ }, [splashScreenState, tryNewDotData]);
+
const isAuthenticated = useMemo(() => !!(session?.authToken ?? null), [session]);
const autoAuthState = useMemo(() => session?.autoAuthState ?? '', [session]);
const shouldInit = isNavigationReady && hasAttemptedToOpenPublicRoom;
- const shouldHideSplash = shouldInit && !isSplashHidden;
+ const shouldHideSplash =
+ shouldInit &&
+ (NativeModules.HybridAppModule ? splashScreenState === CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN && isAuthenticated : splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE);
const initializeClient = () => {
if (!Visibility.isVisible()) {
@@ -145,17 +150,9 @@ function Expensify({
}, []);
const onSplashHide = useCallback(() => {
- setIsSplashHidden(true);
+ setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN);
Performance.markEnd(CONST.TIMING.SIDEBAR_LOADED);
- }, []);
-
- const contextValue = useMemo(
- () => ({
- isSplashHidden,
- setIsSplashHidden,
- }),
- [isSplashHidden, setIsSplashHidden],
- );
+ }, [setSplashScreenState]);
useLayoutEffect(() => {
// Initialize this client as being an active client
@@ -177,24 +174,22 @@ function Expensify({
useEffect(() => {
setTimeout(() => {
- BootSplash.getVisibilityStatus().then((status) => {
- const appState = AppState.currentState;
- Log.info('[BootSplash] splash screen status', false, {appState, status});
-
- if (status === 'visible') {
- const propsToLog: Omit = {
- isCheckingPublicRoom,
- updateRequired,
- updateAvailable,
- isSidebarLoaded,
- screenShareRequest,
- focusModeNotification,
- isAuthenticated,
- lastVisitedPath,
- };
- Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false);
- }
- });
+ const appState = AppState.currentState;
+ Log.info('[BootSplash] splash screen status', false, {appState, splashScreenState});
+
+ if (splashScreenState === CONST.BOOT_SPLASH_STATE.VISIBLE) {
+ const propsToLog: Omit = {
+ isCheckingPublicRoom,
+ updateRequired,
+ updateAvailable,
+ isSidebarLoaded,
+ screenShareRequest,
+ focusModeNotification,
+ isAuthenticated,
+ lastVisitedPath,
+ };
+ Log.alert('[BootSplash] splash screen is still visible', {propsToLog}, false);
+ }
}, 30 * 1000);
// This timer is set in the native layer when launching the app and we stop it here so we can measure how long
@@ -304,18 +299,15 @@ function Expensify({
{hasAttemptedToOpenPublicRoom && (
-
-
-
+
)}
- {/* HybridApp has own middleware to hide SplashScreen */}
- {!NativeModules.HybridAppModule && shouldHideSplash && }
+ {shouldHideSplash && }
);
}
@@ -349,5 +341,3 @@ export default withOnyx({
key: ONYXKEYS.LAST_VISITED_PATH,
},
})(Expensify);
-
-export {SplashScreenHiddenContext};
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index a276f93403bd..d2a0372fd9c7 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -392,6 +392,9 @@ const ONYXKEYS = {
/** Stores the information about the state of issuing a new card */
ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard',
+ /** Stores the information about the state of assigning a company card */
+ ASSIGN_CARD: 'assignCard',
+
/** Stores the information if mobile selection mode is active */
MOBILE_SELECTION_MODE: 'mobileSelectionMode',
@@ -615,6 +618,8 @@ const ONYXKEYS = {
SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft',
ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard',
ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft',
+ ASSIGN_CARD_FORM: 'assignCard',
+ ASSIGN_CARD_FORM_DRAFT: 'assignCardDraft',
EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName',
EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft',
EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit',
@@ -712,6 +717,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm;
[ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm;
[ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm;
+ [ONYXKEYS.FORMS.ASSIGN_CARD_FORM]: FormTypes.AssignCardForm;
[ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm;
[ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm;
[ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm;
@@ -908,6 +914,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings;
[ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates;
[ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
+ [ONYXKEYS.ASSIGN_CARD]: OnyxTypes.AssignCard;
[ONYXKEYS.MOBILE_SELECTION_MODE]: OnyxTypes.MobileSelectionMode;
[ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string;
[ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 32181bca2bd8..89023063ad8f 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -910,6 +910,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/company-cards',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards` as const,
},
+ WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: {
+ route: 'settings/workspaces/:policyID/company-cards/:feed/assign-card',
+ getRoute: (policyID: string, feed: string) => `settings/workspaces/${policyID}/company-cards/${feed}/assign-card` as const,
+ },
WORKSPACE_EXPENSIFY_CARD_DETAILS: {
route: 'settings/workspaces/:policyID/expensify-card/:cardID',
getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo),
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 829da0575d0a..db790dd389c3 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -368,6 +368,7 @@ const SCREENS = {
RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate',
RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit',
COMPANY_CARDS: 'Workspace_CompanyCards',
+ COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard',
COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed',
COMPANY_CARDS_SETTINGS: 'Workspace_CompanyCards_Settings',
COMPANY_CARDS_SETTINGS_FEED_NAME: 'Workspace_CompanyCards_Settings_Feed_Name',
diff --git a/src/SplashScreenStateContext.tsx b/src/SplashScreenStateContext.tsx
new file mode 100644
index 000000000000..90a858f70c42
--- /dev/null
+++ b/src/SplashScreenStateContext.tsx
@@ -0,0 +1,34 @@
+import React, {useContext, useMemo, useState} from 'react';
+import type {ValueOf} from 'type-fest';
+import CONST from './CONST';
+import type ChildrenProps from './types/utils/ChildrenProps';
+
+type SplashScreenStateContextType = {
+ splashScreenState: ValueOf;
+ setSplashScreenState: React.Dispatch>>;
+};
+
+const SplashScreenStateContext = React.createContext({
+ splashScreenState: CONST.BOOT_SPLASH_STATE.VISIBLE,
+ setSplashScreenState: () => {},
+});
+
+function SplashScreenStateContextProvider({children}: ChildrenProps) {
+ const [splashScreenState, setSplashScreenState] = useState>(CONST.BOOT_SPLASH_STATE.VISIBLE);
+ const splashScreenStateContext = useMemo(
+ () => ({
+ splashScreenState,
+ setSplashScreenState,
+ }),
+ [splashScreenState],
+ );
+
+ return {children};
+}
+
+function useSplashScreenStateContext() {
+ return useContext(SplashScreenStateContext);
+}
+
+export default SplashScreenStateContext;
+export {SplashScreenStateContextProvider, useSplashScreenStateContext};
diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx
index c58bb3a09f29..0abc55088647 100644
--- a/src/components/ButtonWithDropdownMenu/index.tsx
+++ b/src/components/ButtonWithDropdownMenu/index.tsx
@@ -110,7 +110,7 @@ function ButtonWithDropdownMenu({
isDisabled={isDisabled || !!selectedItem?.disabled}
isLoading={isLoading}
shouldRemoveRightBorderRadius
- style={[styles.flex1, styles.pr0]}
+ style={isSplitButton ? [styles.flex1, styles.pr0] : {}}
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL}
diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx
index 75f65a06a2e6..f56441316f7c 100644
--- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx
+++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx
@@ -4,6 +4,7 @@ import BootSplash from '@libs/BootSplash';
import GenericErrorPage from '@pages/ErrorPage/GenericErrorPage';
import UpdateRequiredView from '@pages/ErrorPage/UpdateRequiredView';
import CONST from '@src/CONST';
+import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
import type {BaseErrorBoundaryProps, LogError} from './types';
/**
@@ -14,10 +15,12 @@ import type {BaseErrorBoundaryProps, LogError} from './types';
function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseErrorBoundaryProps) {
const [errorContent, setErrorContent] = useState('');
+ const {setSplashScreenState} = useSplashScreenStateContext();
+
const catchError = (errorObject: Error, errorInfo: React.ErrorInfo) => {
logError(errorMessage, errorObject, JSON.stringify(errorInfo));
// We hide the splash screen since the error might happened during app init
- BootSplash.hide();
+ BootSplash.hide().then(() => setSplashScreenState(CONST.BOOT_SPLASH_STATE.HIDDEN));
setErrorContent(errorObject.message);
};
const updateRequired = errorContent === CONST.ERROR.UPDATE_REQUIRED;
diff --git a/src/components/HybridAppMiddleware/index.ios.tsx b/src/components/HybridAppMiddleware/index.ios.tsx
index aee837e02dea..0fb61fb9ad7a 100644
--- a/src/components/HybridAppMiddleware/index.ios.tsx
+++ b/src/components/HybridAppMiddleware/index.ios.tsx
@@ -1,21 +1,13 @@
import type React from 'react';
-import {useContext, useEffect, useRef, useState} from 'react';
+import {useEffect} from 'react';
import {NativeEventEmitter, NativeModules} from 'react-native';
import type {NativeModule} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
-import {InitialURLContext} from '@components/InitialURLContextProvider';
-import useExitTo from '@hooks/useExitTo';
-import useSplashScreen from '@hooks/useSplashScreen';
-import BootSplash from '@libs/BootSplash';
import Log from '@libs/Log';
-import Navigation from '@libs/Navigation/Navigation';
-import * as SessionUtils from '@libs/SessionUtils';
-import * as Welcome from '@userActions/Welcome';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {HybridAppRoute, Route} from '@src/ROUTES';
-import ROUTES from '@src/ROUTES';
+import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
import type {TryNewDot} from '@src/types/onyx';
type HybridAppMiddlewareProps = {
@@ -38,33 +30,10 @@ const onboardingStatusSelector = (tryNewDot: OnyxEntry) => {
* It is crucial to make transitions between OldDot and NewDot look smooth.
* The middleware assumes that the entry point for HybridApp is the /transition route.
*/
-function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) {
- const {isSplashHidden, setIsSplashHidden} = useSplashScreen();
- const [startedTransition, setStartedTransition] = useState(false);
- const [finishedTransition, setFinishedTransition] = useState(false);
-
- const initialURL = useContext(InitialURLContext);
- const exitToParam = useExitTo();
- const [exitTo, setExitTo] = useState();
-
- const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false});
- const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
+function HybridAppMiddleware({children}: HybridAppMiddlewareProps) {
+ const {setSplashScreenState} = useSplashScreenStateContext();
const [completedHybridAppOnboarding] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {selector: onboardingStatusSelector});
- const maxTimeoutRef = useRef(null);
-
- // We need to ensure that the BootSplash is always hidden after a certain period.
- useEffect(() => {
- if (!NativeModules.HybridAppModule) {
- return;
- }
-
- maxTimeoutRef.current = setTimeout(() => {
- Log.info('[HybridApp] Forcing transition due to unknown problem', true);
- setStartedTransition(true);
- setExitTo(ROUTES.HOME);
- }, 3000);
- }, []);
/**
* This useEffect tracks changes of `nvp_tryNewDot` value.
* We propagate it from OldDot to NewDot with native method due to limitations of old app.
@@ -94,79 +63,13 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps
const HybridAppEvents = new NativeEventEmitter(NativeModules.HybridAppModule as unknown as NativeModule);
const listener = HybridAppEvents.addListener(CONST.EVENTS.ON_RETURN_TO_OLD_DOT, () => {
Log.info('[HybridApp] `onReturnToOldDot` event received. Resetting state of HybridAppMiddleware', true);
- setIsSplashHidden(false);
- setStartedTransition(false);
- setFinishedTransition(false);
- setExitTo(undefined);
+ setSplashScreenState(CONST.BOOT_SPLASH_STATE.VISIBLE);
});
return () => {
listener.remove();
};
- }, [setIsSplashHidden]);
-
- // Save `exitTo` when we reach /transition route.
- // `exitTo` should always exist during OldDot -> NewDot transitions.
- useEffect(() => {
- if (!NativeModules.HybridAppModule || !exitToParam || exitTo) {
- return;
- }
-
- Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam});
- setExitTo(exitToParam);
-
- Log.info(`[HybridApp] Started transition`, true);
- setStartedTransition(true);
- }, [exitTo, exitToParam]);
-
- useEffect(() => {
- if (!startedTransition || finishedTransition) {
- return;
- }
-
- const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL;
- const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail);
-
- // We need to wait with navigating to exitTo until all login-related actions are complete.
- if (!authenticated || isLoggingInAsNewUser || isAccountLoading) {
- return;
- }
-
- if (exitTo) {
- Navigation.isNavigationReady().then(() => {
- // We need to remove /transition from route history.
- // `useExitTo` returns undefined for routes other than /transition.
- if (exitToParam) {
- Log.info('[HybridApp] Removing /transition route from history', true);
- Navigation.goBack();
- }
-
- Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo});
- Navigation.navigate(Navigation.parseHybridAppUrl(exitTo));
- setExitTo(undefined);
-
- setTimeout(() => {
- Log.info('[HybridApp] Setting `finishedTransition` to true', true);
- setFinishedTransition(true);
- }, CONST.SCREEN_TRANSITION_END_TIMEOUT);
- });
- }
- }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]);
-
- useEffect(() => {
- if (!finishedTransition || isSplashHidden) {
- return;
- }
-
- Log.info('[HybridApp] Finished transition, hiding BootSplash', true);
- BootSplash.hide().then(() => {
- setIsSplashHidden(true);
- if (authenticated) {
- Log.info('[HybridApp] Handling onboarding flow', true);
- Welcome.handleHybridAppOnboarding();
- }
- });
- }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]);
+ }, [setSplashScreenState]);
return children;
}
diff --git a/src/components/HybridAppMiddleware/index.tsx b/src/components/HybridAppMiddleware/index.tsx
index bb5d7803e52e..1ebe1347df8e 100644
--- a/src/components/HybridAppMiddleware/index.tsx
+++ b/src/components/HybridAppMiddleware/index.tsx
@@ -1,24 +1,13 @@
import type React from 'react';
-import {useContext, useEffect, useRef, useState} from 'react';
+import {useEffect} from 'react';
import {NativeModules} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
-import {InitialURLContext} from '@components/InitialURLContextProvider';
-import useExitTo from '@hooks/useExitTo';
-import useSplashScreen from '@hooks/useSplashScreen';
-import BootSplash from '@libs/BootSplash';
import Log from '@libs/Log';
-import Navigation from '@libs/Navigation/Navigation';
-import * as SessionUtils from '@libs/SessionUtils';
-import * as Welcome from '@userActions/Welcome';
-import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {HybridAppRoute, Route} from '@src/ROUTES';
-import ROUTES from '@src/ROUTES';
import type {TryNewDot} from '@src/types/onyx';
type HybridAppMiddlewareProps = {
- authenticated: boolean;
children: React.ReactNode;
};
@@ -37,33 +26,9 @@ const onboardingStatusSelector = (tryNewDot: OnyxEntry) => {
* It is crucial to make transitions between OldDot and NewDot look smooth.
* The middleware assumes that the entry point for HybridApp is the /transition route.
*/
-function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps) {
- const {isSplashHidden, setIsSplashHidden} = useSplashScreen();
- const [startedTransition, setStartedTransition] = useState(false);
- const [finishedTransition, setFinishedTransition] = useState(false);
-
- const initialURL = useContext(InitialURLContext);
- const exitToParam = useExitTo();
- const [exitTo, setExitTo] = useState();
-
- const [isAccountLoading] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => account?.isLoading ?? false});
- const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email});
+function HybridAppMiddleware({children}: HybridAppMiddlewareProps) {
const [completedHybridAppOnboarding] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, {selector: onboardingStatusSelector});
- const maxTimeoutRef = useRef(null);
-
- // We need to ensure that the BootSplash is always hidden after a certain period.
- useEffect(() => {
- if (!NativeModules.HybridAppModule) {
- return;
- }
-
- maxTimeoutRef.current = setTimeout(() => {
- Log.info('[HybridApp] Forcing transition due to unknown problem', true);
- setStartedTransition(true);
- setExitTo(ROUTES.HOME);
- }, 3000);
- }, []);
/**
* This useEffect tracks changes of `nvp_tryNewDot` value.
* We propagate it from OldDot to NewDot with native method due to limitations of old app.
@@ -77,71 +42,6 @@ function HybridAppMiddleware({children, authenticated}: HybridAppMiddlewareProps
NativeModules.HybridAppModule.completeOnboarding(completedHybridAppOnboarding);
}, [completedHybridAppOnboarding]);
- // Save `exitTo` when we reach /transition route.
- // `exitTo` should always exist during OldDot -> NewDot transitions.
- useEffect(() => {
- if (!NativeModules.HybridAppModule || !exitToParam || exitTo) {
- return;
- }
-
- Log.info('[HybridApp] Saving `exitTo` for later', true, {exitTo: exitToParam});
- setExitTo(exitToParam);
-
- Log.info(`[HybridApp] Started transition`, true);
- setStartedTransition(true);
- }, [exitTo, exitToParam]);
-
- useEffect(() => {
- if (!startedTransition || finishedTransition) {
- return;
- }
-
- const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initialURL ?? ''}` : initialURL;
- const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail);
-
- // We need to wait with navigating to exitTo until all login-related actions are complete.
- if (!authenticated || isLoggingInAsNewUser || isAccountLoading) {
- return;
- }
-
- if (exitTo) {
- Navigation.isNavigationReady().then(() => {
- // We need to remove /transition from route history.
- // `useExitTo` returns undefined for routes other than /transition.
- if (exitToParam && Navigation.getActiveRoute().includes(ROUTES.TRANSITION_BETWEEN_APPS)) {
- Log.info('[HybridApp] Removing /transition route from history', true);
- Navigation.goBack();
- }
-
- if (exitTo !== ROUTES.HOME) {
- Log.info('[HybridApp] Navigating to `exitTo` route', true, {exitTo});
- Navigation.navigate(Navigation.parseHybridAppUrl(exitTo));
- }
- setExitTo(undefined);
-
- setTimeout(() => {
- Log.info('[HybridApp] Setting `finishedTransition` to true', true);
- setFinishedTransition(true);
- }, CONST.SCREEN_TRANSITION_END_TIMEOUT);
- });
- }
- }, [authenticated, exitTo, exitToParam, finishedTransition, initialURL, isAccountLoading, sessionEmail, startedTransition]);
-
- useEffect(() => {
- if (!finishedTransition || isSplashHidden) {
- return;
- }
-
- Log.info('[HybridApp] Finished transition, hiding BootSplash', true);
- BootSplash.hide().then(() => {
- setIsSplashHidden(true);
- if (authenticated) {
- Log.info('[HybridApp] Handling onboarding flow', true);
- Welcome.handleHybridAppOnboarding();
- }
- });
- }, [authenticated, finishedTransition, isSplashHidden, setIsSplashHidden]);
-
return children;
}
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 70cd0168aba8..0616794a8e3a 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -9,6 +9,7 @@ import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg';
import BankArrowPink from '@assets/images/product-illustrations/bank-arrow--pink.svg';
import BankMouseGreen from '@assets/images/product-illustrations/bank-mouse--green.svg';
import BankUserGreen from '@assets/images/product-illustrations/bank-user--green.svg';
+import BrokenMagnifyingGlass from '@assets/images/product-illustrations/broken-magnifying-glass.svg';
import ConciergeBlue from '@assets/images/product-illustrations/concierge--blue.svg';
import ConciergeExclamation from '@assets/images/product-illustrations/concierge--exclamation.svg';
import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg';
@@ -121,6 +122,7 @@ export {
BankMouseGreen,
BankUserGreen,
BigRocket,
+ BrokenMagnifyingGlass,
ChatBubbles,
CoffeeMug,
ConciergeBlue,
diff --git a/src/components/ImportColumn.tsx b/src/components/ImportColumn.tsx
index be82fb9d22d6..f25082f94474 100644
--- a/src/components/ImportColumn.tsx
+++ b/src/components/ImportColumn.tsx
@@ -176,7 +176,7 @@ function ImportColumn({column, columnName, columnRoles, columnIndex}: ImportColu
return (
- {columnHeader}
+ {columnHeader}
{
- Navigation.navigate(goTo);
- });
+ setSpreadsheetData(data as string[][])
+ .then(() => {
+ Navigation.navigate(goTo);
+ })
+ .catch(() => {
+ setUploadFileError(true, 'spreadsheet.importFailedTitle', 'spreadsheet.invalidFileMessage');
+ });
})
.finally(() => {
setIsReadingFIle(false);
diff --git a/src/components/InitialURLContextProvider.tsx b/src/components/InitialURLContextProvider.tsx
index 4aa76edd3e62..85ad54ca6c94 100644
--- a/src/components/InitialURLContextProvider.tsx
+++ b/src/components/InitialURLContextProvider.tsx
@@ -1,10 +1,21 @@
-import React, {createContext, useEffect, useState} from 'react';
+import React, {createContext, useEffect, useMemo, useState} from 'react';
import type {ReactNode} from 'react';
import {Linking} from 'react-native';
+import {signInAfterTransitionFromOldDot} from '@libs/actions/Session';
+import CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';
+import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
+
+type InitialUrlContextType = {
+ initialURL: Route | undefined;
+ setInitialURL: React.Dispatch>;
+};
/** Initial url that will be opened when NewDot is embedded into Hybrid App. */
-const InitialURLContext = createContext(undefined);
+const InitialURLContext = createContext({
+ initialURL: undefined,
+ setInitialURL: () => {},
+});
type InitialURLContextProviderProps = {
/** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */
@@ -15,17 +26,30 @@ type InitialURLContextProviderProps = {
};
function InitialURLContextProvider({children, url}: InitialURLContextProviderProps) {
- const [initialURL, setInitialURL] = useState(url);
+ const [initialURL, setInitialURL] = useState(url);
+ const {setSplashScreenState} = useSplashScreenStateContext();
+
useEffect(() => {
if (url) {
- setInitialURL(url);
+ const route = signInAfterTransitionFromOldDot(url);
+ setInitialURL(route);
+ setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN);
return;
}
Linking.getInitialURL().then((initURL) => {
setInitialURL(initURL as Route);
});
- }, [url]);
- return {children};
+ }, [setSplashScreenState, url]);
+
+ const initialUrlContext = useMemo(
+ () => ({
+ initialURL,
+ setInitialURL,
+ }),
+ [initialURL],
+ );
+
+ return {children};
}
InitialURLContextProvider.displayName = 'InitialURLContextProvider';
diff --git a/src/components/InteractiveStepWrapper.tsx b/src/components/InteractiveStepWrapper.tsx
new file mode 100644
index 000000000000..6ffe00b9bd5d
--- /dev/null
+++ b/src/components/InteractiveStepWrapper.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import {View} from 'react-native';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import HeaderWithBackButton from './HeaderWithBackButton';
+import InteractiveStepSubHeader from './InteractiveStepSubHeader';
+import ScreenWrapper from './ScreenWrapper';
+
+type InteractiveStepWrapperProps = {
+ // Step content
+ children: React.ReactNode;
+
+ // ID of the wrapper
+ wrapperID: string;
+
+ // Function to handle back button press
+ handleBackButtonPress: () => void;
+
+ // Title of the back button header
+ headerTitle: string;
+
+ // Index of the highlighted step
+ startStepIndex?: number;
+
+ // Array of step names
+ stepNames?: readonly string[];
+};
+
+function InteractiveStepWrapper({children, wrapperID, handleBackButtonPress, headerTitle, startStepIndex, stepNames}: InteractiveStepWrapperProps) {
+ const styles = useThemeStyles();
+
+ return (
+
+
+ {stepNames && (
+
+
+
+ )}
+ {children}
+
+ );
+}
+
+InteractiveStepWrapper.displayName = 'InteractiveStepWrapper';
+
+export default InteractiveStepWrapper;
diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx
index dd46b33a8400..7c85837644a2 100644
--- a/src/components/Lottie/index.tsx
+++ b/src/components/Lottie/index.tsx
@@ -6,8 +6,9 @@ import {View} from 'react-native';
import type DotLottieAnimation from '@components/LottieAnimations/types';
import useAppState from '@hooks/useAppState';
import useNetwork from '@hooks/useNetwork';
-import useSplashScreen from '@hooks/useSplashScreen';
import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
type Props = {
source: DotLottieAnimation;
@@ -15,7 +16,7 @@ type Props = {
function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef) {
const appState = useAppState();
- const {isSplashHidden} = useSplashScreen();
+ const {splashScreenState} = useSplashScreenStateContext();
const styles = useThemeStyles();
const [isError, setIsError] = React.useState(false);
@@ -33,7 +34,7 @@ function Lottie({source, webStyle, ...props}: Props, ref: ForwardedRef;
}
diff --git a/src/hooks/useAutoFocusInput.ts b/src/hooks/useAutoFocusInput.ts
index 26f045ebd579..5509d3635299 100644
--- a/src/hooks/useAutoFocusInput.ts
+++ b/src/hooks/useAutoFocusInput.ts
@@ -1,10 +1,10 @@
import {useFocusEffect} from '@react-navigation/native';
-import {useCallback, useContext, useEffect, useRef, useState} from 'react';
+import {useCallback, useEffect, useRef, useState} from 'react';
import type {RefObject} from 'react';
import type {TextInput} from 'react-native';
import {InteractionManager} from 'react-native';
import CONST from '@src/CONST';
-import * as Expensify from '@src/Expensify';
+import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
type UseAutoFocusInput = {
inputCallbackRef: (ref: TextInput | null) => void;
@@ -15,13 +15,13 @@ export default function useAutoFocusInput(): UseAutoFocusInput {
const [isInputInitialized, setIsInputInitialized] = useState(false);
const [isScreenTransitionEnded, setIsScreenTransitionEnded] = useState(false);
- const {isSplashHidden} = useContext(Expensify.SplashScreenHiddenContext);
+ const {splashScreenState} = useSplashScreenStateContext();
const inputRef = useRef(null);
const focusTimeoutRef = useRef(null);
useEffect(() => {
- if (!isScreenTransitionEnded || !isInputInitialized || !inputRef.current || !isSplashHidden) {
+ if (!isScreenTransitionEnded || !isInputInitialized || !inputRef.current || splashScreenState !== CONST.BOOT_SPLASH_STATE.HIDDEN) {
return;
}
const focusTaskHandle = InteractionManager.runAfterInteractions(() => {
@@ -32,7 +32,7 @@ export default function useAutoFocusInput(): UseAutoFocusInput {
return () => {
focusTaskHandle.cancel();
};
- }, [isScreenTransitionEnded, isInputInitialized, isSplashHidden]);
+ }, [isScreenTransitionEnded, isInputInitialized, splashScreenState]);
useFocusEffect(
useCallback(() => {
diff --git a/src/hooks/useExitTo.ts b/src/hooks/useExitTo.ts
deleted file mode 100644
index 74226453d3f6..000000000000
--- a/src/hooks/useExitTo.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
-import type {PublicScreensParamList, RootStackParamList} from '@libs/Navigation/types';
-import SCREENS from '@src/SCREENS';
-
-export default function useExitTo() {
- const activeRouteParams = useNavigationState((state) => {
- const focusedRoute = findFocusedRoute(state);
-
- if (focusedRoute?.name !== SCREENS.TRANSITION_BETWEEN_APPS) {
- return undefined;
- }
-
- return focusedRoute?.params as PublicScreensParamList[typeof SCREENS.TRANSITION_BETWEEN_APPS];
- });
-
- return activeRouteParams?.exitTo;
-}
diff --git a/src/hooks/useSplashScreen.ts b/src/hooks/useSplashScreen.ts
deleted file mode 100644
index 8838ac1289c7..000000000000
--- a/src/hooks/useSplashScreen.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import {useContext} from 'react';
-import {SplashScreenHiddenContext} from '@src/Expensify';
-
-type SplashScreenHiddenContextType = {isSplashHidden: boolean; setIsSplashHidden: React.Dispatch>};
-
-export default function useSplashScreen() {
- const {isSplashHidden, setIsSplashHidden} = useContext(SplashScreenHiddenContext) as SplashScreenHiddenContextType;
- return {isSplashHidden, setIsSplashHidden};
-}
-
-export type {SplashScreenHiddenContextType};
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 66d1e7c73c90..e14017c420e6 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -10,6 +10,7 @@ import type {
AlreadySignedInParams,
ApprovalWorkflowErrorParams,
ApprovedAmountParams,
+ AssignCardParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartTwo,
@@ -686,6 +687,8 @@ export default {
importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'),
importFailedTitle: 'Import failed',
importFailedDescription: 'Please ensure all fields are filled out correctly and try again. If the problem persists, please reach out to Concierge.',
+ invalidFileMessage:
+ 'The file you uploaded is either empty or contains invalid data. Please ensure that the file is correctly formatted and contains the necessary information before uploading it again.',
},
receipt: {
upload: 'Upload receipt',
@@ -2792,6 +2795,23 @@ export default {
assignCard: 'Assign card',
cardNumber: 'Card number',
customFeed: 'Custom feed',
+ whoNeedsCardAssigned: 'Who needs a card assigned?',
+ chooseCard: 'Choose a card',
+ chooseCardFor: ({assignee, feed}: AssignCardParams) => `Choose a card for ${assignee} from the ${feed} cards feed.`,
+ noActiveCards: 'No active cards on this feed',
+ somethingMightBeBroken: 'Or something might be broken. Either way, if you have any questions, just',
+ contactConcierge: 'contact Concierge',
+ chooseTransactionStartDate: 'Choose a transaction start date',
+ startDateDescription: 'We will import all transaction from this date onwards. If no date is specified, we’ll go as far back as your bank allows.',
+ fromTheBeginning: 'From the beginning',
+ customStartDate: 'Custom start date',
+ letsDoubleCheck: 'Let’s double check that everything looks right.',
+ confirmationDescription: 'We’ll begin importing transactions immediately.',
+ cardholder: 'Cardholder',
+ card: 'Card',
+ startTransactionDate: 'Start transaction date',
+ cardName: 'Card name',
+ assignedYouCard: (assigner: string) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`,
},
expensifyCard: {
issueAndManageCards: 'Issue and manage your Expensify Cards',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 54402fd1d6d5..b0dad8b3981c 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -8,6 +8,7 @@ import type {
AlreadySignedInParams,
ApprovalWorkflowErrorParams,
ApprovedAmountParams,
+ AssignCardParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartTwo,
@@ -679,6 +680,8 @@ export default {
importFailedDescription: 'Por favor, asegúrate de que todos los campos estén llenos correctamente e inténtalo de nuevo. Si el problema persiste, por favor contacta a Concierge.',
importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `Se han agregado ${categories} categorÃas.` : 'Se ha agregado 1 categorÃa.'),
importSuccessfullTitle: 'Importar categorÃas',
+ invalidFileMessage:
+ 'El archivo que ha cargado está vacÃo o contiene datos no válidos. Asegúrese de que el archivo tiene el formato correcto y contiene la información necesaria antes de volver a cargarlo.',
},
receipt: {
upload: 'Subir recibo',
@@ -2839,6 +2842,23 @@ export default {
assignCard: 'Asignar tarjeta',
cardNumber: 'Número de la tarjeta',
customFeed: 'Fuente personalizada',
+ whoNeedsCardAssigned: '¿Quién necesita una tarjeta?',
+ chooseCard: 'Elige una tarjeta',
+ chooseCardFor: ({assignee, feed}: AssignCardParams) => `Elige una tarjeta para ${assignee} del feed de tarjetas ${feed}.`,
+ noActiveCards: 'No hay tarjetas activas en este feed',
+ somethingMightBeBroken: 'O algo podrÃa estar roto. De cualquier manera, si tienes alguna pregunta,',
+ contactConcierge: 'contacta a Concierge',
+ chooseTransactionStartDate: 'Elige una fecha de inicio de transacciones',
+ startDateDescription: 'Importaremos todas las transacciones desde esta fecha en adelante. Si no se especifica una fecha, iremos tan atrás como lo permita tu banco.',
+ fromTheBeginning: 'Desde el principio',
+ customStartDate: 'Fecha de inicio personalizada',
+ letsDoubleCheck: 'Verifiquemos que todo esté bien.',
+ confirmationDescription: 'Comenzaremos a importar transacciones inmediatamente.',
+ cardholder: 'Titular de la tarjeta',
+ card: 'Tarjeta',
+ startTransactionDate: 'Fecha de inicio de transacciones',
+ cardName: 'Nombre de la tarjeta',
+ assignedYouCard: (assigner: string) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`,
},
expensifyCard: {
issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index fb396a3f64ea..f953cb17255b 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -361,6 +361,11 @@ type ApprovalWorkflowErrorParams = {
name2: string;
};
+type AssignCardParams = {
+ assignee: string;
+ feed: string;
+};
+
export type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -485,4 +490,5 @@ export type {
RemoveMembersWarningPrompt,
DeleteExpenseTranslationParams,
ApprovalWorkflowErrorParams,
+ AssignCardParams,
};
diff --git a/src/libs/BootSplash/index.native.ts b/src/libs/BootSplash/index.native.ts
index 9d472aec4a96..3c1ccb2ce7ce 100644
--- a/src/libs/BootSplash/index.native.ts
+++ b/src/libs/BootSplash/index.native.ts
@@ -10,7 +10,6 @@ function hide(): Promise {
export default {
hide,
- getVisibilityStatus: BootSplash.getVisibilityStatus,
logoSizeRatio: BootSplash.logoSizeRatio || 1,
navigationBarHeight: BootSplash.navigationBarHeight || 0,
};
diff --git a/src/libs/BootSplash/index.ts b/src/libs/BootSplash/index.ts
index 774c5f7b06ac..a859f545abca 100644
--- a/src/libs/BootSplash/index.ts
+++ b/src/libs/BootSplash/index.ts
@@ -1,5 +1,4 @@
import Log from '@libs/Log';
-import type {VisibilityStatus} from './types';
function resolveAfter(delay: number): Promise {
return new Promise((resolve) => {
@@ -25,13 +24,8 @@ function hide(): Promise {
});
}
-function getVisibilityStatus(): Promise {
- return Promise.resolve(document.getElementById('splash') ? 'visible' : 'hidden');
-}
-
export default {
hide,
- getVisibilityStatus,
logoSizeRatio: 1,
navigationBarHeight: 0,
};
diff --git a/src/libs/BootSplash/types.ts b/src/libs/BootSplash/types.ts
index b50b5a3397aa..106535453ed8 100644
--- a/src/libs/BootSplash/types.ts
+++ b/src/libs/BootSplash/types.ts
@@ -4,7 +4,6 @@ type BootSplashModule = {
logoSizeRatio: number;
navigationBarHeight: number;
hide: () => Promise;
- getVisibilityStatus: () => Promise;
};
export type {BootSplashModule, VisibilityStatus};
diff --git a/src/libs/Middleware/Logging.ts b/src/libs/Middleware/Logging.ts
index d327dd06becc..dc449270ff88 100644
--- a/src/libs/Middleware/Logging.ts
+++ b/src/libs/Middleware/Logging.ts
@@ -6,6 +6,34 @@ import type Request from '@src/types/onyx/Request';
import type Response from '@src/types/onyx/Response';
import type Middleware from './types';
+function getCircularReplacer() {
+ const ancestors: unknown[] = [];
+ return function (this: unknown, key: string, value: unknown): unknown {
+ if (typeof value !== 'object' || value === null) {
+ return value;
+ }
+ // `this` is the object that value is contained in, i.e the direct parent
+ // eslint-disable-next-line no-invalid-this
+ while (ancestors.length > 0 && ancestors.at(-1) !== this) {
+ ancestors.pop();
+ }
+ if (ancestors.includes(value)) {
+ return '[Circular]';
+ }
+ ancestors.push(value);
+ return value;
+ };
+}
+
+function serializeLoggingData | undefined>(logData: T): T | null {
+ try {
+ return JSON.parse(JSON.stringify(logData, getCircularReplacer())) as T;
+ } catch (error) {
+ Log.hmmm('Failed to serialize log data', {error});
+ return null;
+ }
+}
+
function logRequestDetails(message: string, request: Request, response?: Response | void) {
// Don't log about log or else we'd cause an infinite loop
if (request.command === 'Log') {
@@ -38,7 +66,10 @@ function logRequestDetails(message: string, request: Request, response?: Respons
* requests because they contain sensitive information.
*/
if (request.command !== 'AuthenticatePusher') {
- extraData.request = request;
+ extraData.request = {
+ ...request,
+ data: serializeLoggingData(request.data),
+ };
extraData.response = response;
}
@@ -125,3 +156,5 @@ const Logging: Middleware = (response, request) => {
};
export default Logging;
+
+export {serializeLoggingData};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 1bd32b1cd8a7..c2a30f20ed56 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -424,6 +424,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/WorkspaceTaxCodePage').default,
[SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsName').default,
[SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default,
+ [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: () => require('../../../../pages/workspace/companyCards/assignCard/AssignCardFeedPage').default,
[SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/expensifyCard/issueNew/IssueNewCardPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default,
diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.tsx b/src/libs/Navigation/AppNavigator/PublicScreens.tsx
index faf6563a0ef3..cfd41a4b1fa0 100644
--- a/src/libs/Navigation/AppNavigator/PublicScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/PublicScreens.tsx
@@ -1,7 +1,9 @@
import {createStackNavigator} from '@react-navigation/stack';
import React from 'react';
+import {NativeModules} from 'react-native';
import type {PublicScreensParamList} from '@navigation/types';
import ConnectionCompletePage from '@pages/ConnectionCompletePage';
+import SessionExpiredPage from '@pages/ErrorPage/SessionExpiredPage';
import LogInWithShortLivedAuthTokenPage from '@pages/LogInWithShortLivedAuthTokenPage';
import AppleSignInDesktopPage from '@pages/signin/AppleSignInDesktopPage';
import GoogleSignInDesktopPage from '@pages/signin/GoogleSignInDesktopPage';
@@ -22,7 +24,7 @@ function PublicScreens() {
{
- if (!NativeModules.HybridAppModule || !initUrl || !initUrl.includes(ROUTES.TRANSITION_BETWEEN_APPS)) {
+ if (!NativeModules.HybridAppModule || !initialURL) {
return;
}
Navigation.isNavigationReady().then(() => {
- Navigation.navigate(initUrl);
+ Navigation.navigate(initialURL);
});
- }, [initUrl]);
+ }, [initialURL]);
if (authenticated) {
const AuthScreens = require('./AuthScreens').default;
diff --git a/src/libs/Navigation/AppNavigator/index.tsx b/src/libs/Navigation/AppNavigator/index.tsx
index e0f1dae94f62..1901a51563e9 100644
--- a/src/libs/Navigation/AppNavigator/index.tsx
+++ b/src/libs/Navigation/AppNavigator/index.tsx
@@ -14,17 +14,17 @@ type AppNavigatorProps = {
};
function AppNavigator({authenticated}: AppNavigatorProps) {
- const initUrl = useContext(InitialURLContext);
+ const {initialURL} = useContext(InitialURLContext);
useEffect(() => {
- if (!NativeModules.HybridAppModule || !initUrl || !initUrl.includes(ROUTES.TRANSITION_BETWEEN_APPS)) {
+ if (!NativeModules.HybridAppModule || !initialURL || !initialURL.includes(ROUTES.TRANSITION_BETWEEN_APPS)) {
return;
}
Navigation.isNavigationReady().then(() => {
- Navigation.navigate(initUrl);
+ Navigation.navigate(initialURL);
});
- }, [initUrl]);
+ }, [initialURL]);
if (authenticated) {
// These are the protected screens and only accessible when an authToken is present
diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx
index 4e9b775cbb8d..ba489d67aeb5 100644
--- a/src/libs/Navigation/NavigationRoot.tsx
+++ b/src/libs/Navigation/NavigationRoot.tsx
@@ -1,6 +1,7 @@
import type {NavigationState} from '@react-navigation/native';
import {DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native';
import React, {useContext, useEffect, useMemo, useRef} from 'react';
+import {NativeModules} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import HybridAppMiddleware from '@components/HybridAppMiddleware';
import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider';
@@ -97,7 +98,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh
// If the user haven't completed the flow, we want to always redirect them to the onboarding flow.
// We also make sure that the user is authenticated.
- if (!hasCompletedGuidedSetupFlow && authenticated && !shouldShowRequire2FAModal) {
+ if (!NativeModules.HybridAppModule && !hasCompletedGuidedSetupFlow && authenticated && !shouldShowRequire2FAModal) {
const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT.route, linkingConfig.config);
return adaptedState;
}
@@ -181,7 +182,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh
}}
>
{/* HybridAppMiddleware needs to have access to navigation ref and SplashScreenHidden context */}
-
+
diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
index 79d90453b676..22db5deaebfb 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -162,7 +162,12 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
],
[SCREENS.WORKSPACE.INVOICES]: [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME, SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE],
- [SCREENS.WORKSPACE.COMPANY_CARDS]: [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME],
+ [SCREENS.WORKSPACE.COMPANY_CARDS]: [
+ SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED,
+ SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS,
+ SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME,
+ SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD,
+ ],
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [
SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW,
SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 86919a114096..cf45126bfc04 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -517,6 +517,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.route,
},
+ [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: {
+ path: ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.route,
+ },
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
path: ROUTES.WORKSPACE_RATE_AND_UNIT.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 6c0a1f3e4ee6..c8c2c0f0e41d 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1143,6 +1143,10 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.COMPANY_CARDS]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: {
+ policyID: string;
+ feed: string;
+ };
[SCREENS.WORKSPACE.WORKFLOWS]: {
policyID: string;
};
diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts
index 88e420dfaf7e..e45e14311d71 100644
--- a/src/libs/WorkflowUtils.ts
+++ b/src/libs/WorkflowUtils.ts
@@ -169,11 +169,21 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = {
*/
approvalWorkflow: ApprovalWorkflow;
+ /**
+ * The previous employee list before the approval workflow was created
+ */
+ previousEmployeeList: PolicyEmployeeList;
+
/**
* Members to remove from the approval workflow
*/
membersToRemove?: Member[];
+ /**
+ * Approvers to remove from the approval workflow
+ */
+ approversToRemove?: Approver[];
+
/**
* Mode to use when converting the approval workflow
*/
@@ -181,7 +191,13 @@ type ConvertApprovalWorkflowToPolicyEmployeesParams = {
};
/** Convert an approval workflow to a list of policy employees */
-function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, membersToRemove, type}: ConvertApprovalWorkflowToPolicyEmployeesParams): PolicyEmployeeList {
+function convertApprovalWorkflowToPolicyEmployees({
+ approvalWorkflow,
+ previousEmployeeList,
+ membersToRemove,
+ approversToRemove,
+ type,
+}: ConvertApprovalWorkflowToPolicyEmployeesParams): PolicyEmployeeList {
const updatedEmployeeList: PolicyEmployeeList = {};
const firstApprover = approvalWorkflow.approvers.at(0);
@@ -191,26 +207,60 @@ function convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, membersToRe
approvalWorkflow.approvers.forEach((approver, index) => {
const nextApprover = approvalWorkflow.approvers.at(index + 1);
+ const forwardsTo = type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : nextApprover?.email ?? '';
+
+ if (previousEmployeeList[approver.email]?.forwardsTo === forwardsTo) {
+ return;
+ }
+
updatedEmployeeList[approver.email] = {
email: approver.email,
- forwardsTo: type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : nextApprover?.email ?? '',
+ forwardsTo,
};
});
approvalWorkflow.members.forEach(({email}) => {
+ const submitsTo = type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : firstApprover.email ?? '';
+
+ if (previousEmployeeList[email]?.submitsTo === submitsTo) {
+ return;
+ }
+
+ if (updatedEmployeeList[email]) {
+ updatedEmployeeList[email].submitsTo = submitsTo;
+ return;
+ }
+
updatedEmployeeList[email] = {
- ...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}),
- submitsTo: type === CONST.APPROVAL_WORKFLOW.TYPE.REMOVE ? '' : firstApprover.email ?? '',
+ email,
+ submitsTo,
};
});
membersToRemove?.forEach(({email}) => {
+ if (updatedEmployeeList[email]) {
+ updatedEmployeeList[email].submitsTo = '';
+ return;
+ }
+
updatedEmployeeList[email] = {
- ...(updatedEmployeeList[email] ? updatedEmployeeList[email] : {email}),
+ email,
submitsTo: '',
};
});
+ approversToRemove?.forEach(({email}) => {
+ if (updatedEmployeeList[email]) {
+ updatedEmployeeList[email].forwardsTo = '';
+ return;
+ }
+
+ updatedEmployeeList[email] = {
+ email,
+ forwardsTo: '',
+ };
+ });
+
return updatedEmployeeList;
}
diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts
new file mode 100644
index 000000000000..1e58ea3b6306
--- /dev/null
+++ b/src/libs/actions/CompanyCards.ts
@@ -0,0 +1,13 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {AssignCard} from '@src/types/onyx/AssignCard';
+
+function setAssignCardStepAndData({data, isEditing, currentStep}: Partial) {
+ Onyx.merge(ONYXKEYS.ASSIGN_CARD, {data, isEditing, currentStep});
+}
+
+function clearAssignCardStepAndData() {
+ Onyx.set(ONYXKEYS.ASSIGN_CARD, {});
+}
+
+export {setAssignCardStepAndData, clearAssignCardStepAndData};
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 81d440a89a49..bd1182698d2b 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -80,7 +80,7 @@ import type {
Transaction,
} from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';
-import type {Attributes, CompanyAddress, CustomUnit, Rate, TaxRate, Unit} from '@src/types/onyx/Policy';
+import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, Rate, TaxRate, Unit} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {buildOptimisticPolicyCategories} from './Category';
@@ -604,6 +604,28 @@ function clearNetSuiteErrorField(policyID: string, fieldName: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuite: {options: {config: {errorFields: {[fieldName]: null}}}}}});
}
+function clearNetSuitePendingField(policyID: string, fieldName: string) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuite: {options: {config: {pendingFields: {[fieldName]: null}}}}}});
+}
+
+function removeNetSuiteCustomFieldByIndex(allRecords: NetSuiteCustomSegment[] | NetSuiteCustomList[], policyID: string, importCustomField: string, valueIndex: number) {
+ // We allow multiple custom list records with the same internalID. Hence it is safe to remove by index.
+ const filteredRecords = allRecords.filter((_, index) => index !== Number(valueIndex));
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {
+ connections: {
+ netsuite: {
+ options: {
+ config: {
+ syncOptions: {
+ [importCustomField]: filteredRecords,
+ },
+ },
+ },
+ },
+ },
+ });
+}
+
function clearSageIntacctErrorField(policyID: string, fieldName: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {intacct: {config: {errorFields: {[fieldName]: null}}}}});
}
@@ -3944,7 +3966,9 @@ export {
clearXeroErrorField,
clearSageIntacctErrorField,
clearNetSuiteErrorField,
+ clearNetSuitePendingField,
clearNetSuiteAutoSyncErrorField,
+ removeNetSuiteCustomFieldByIndex,
clearWorkspaceReimbursementErrors,
setWorkspaceCurrencyDefault,
setForeignCurrencyDefault,
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index e905464e551f..1d7e695fa2e3 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -39,6 +39,7 @@ import * as SessionUtils from '@libs/SessionUtils';
import Timers from '@libs/Timers';
import {hideContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import {KEYS_TO_PRESERVE, openApp} from '@userActions/App';
+import * as App from '@userActions/App';
import * as Device from '@userActions/Device';
import * as PriorityMode from '@userActions/PriorityMode';
import redirectToSignIn from '@userActions/SignInRedirect';
@@ -460,6 +461,32 @@ function signUpUser() {
API.write(WRITE_COMMANDS.SIGN_UP_USER, params, {optimisticData, successData, failureData});
}
+function signInAfterTransitionFromOldDot(transitionURL: string) {
+ const [route, queryParams] = transitionURL.split('?');
+
+ const {email, authToken, accountID, autoGeneratedLogin, autoGeneratedPassword, clearOnyxOnStart} = Object.fromEntries(
+ queryParams.split('&').map((param) => {
+ const [key, value] = param.split('=');
+ return [key, value];
+ }),
+ );
+
+ const setSessionDataAndOpenApp = () => {
+ Onyx.multiSet({
+ [ONYXKEYS.SESSION]: {email, authToken, accountID: Number(accountID)},
+ [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
+ }).then(App.openApp);
+ };
+
+ if (clearOnyxOnStart === 'true') {
+ Onyx.clear(KEYS_TO_PRESERVE).then(setSessionDataAndOpenApp);
+ } else {
+ setSessionDataAndOpenApp();
+ }
+
+ return route as Route;
+}
+
/**
* Given an idToken from Sign in with Apple, checks the API to see if an account
* exists for that email address and signs the user in if so.
@@ -1094,4 +1121,5 @@ export {
isSupportAuthToken,
hasStashedSession,
signUpUser,
+ signInAfterTransitionFromOldDot,
};
diff --git a/src/libs/actions/Workflow.ts b/src/libs/actions/Workflow.ts
index 946d7682675d..5a83f3c9cf77 100644
--- a/src/libs/actions/Workflow.ts
+++ b/src/libs/actions/Workflow.ts
@@ -54,7 +54,7 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
const previousEmployeeList = {...policy.employeeList};
const previousApprovalMode = policy.approvalMode;
- const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.CREATE});
+ const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.CREATE});
const optimisticData: OnyxUpdate[] = [
{
@@ -111,7 +111,7 @@ function createApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
API.write(WRITE_COMMANDS.CREATE_WORKSPACE_APPROVAL, parameters, {optimisticData, failureData, successData});
}
-function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWorkflow, membersToRemove: Member[]) {
+function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWorkflow, membersToRemove: Member[], approversToRemove: Approver[]) {
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`];
if (!authToken || !policy) {
@@ -121,7 +121,13 @@ function updateApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
const previousDefaultApprover = policy.approver ?? policy.owner;
const newDefaultApprover = approvalWorkflow.isDefault ? approvalWorkflow.approvers[0].email : undefined;
const previousEmployeeList = {...policy.employeeList};
- const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.UPDATE, membersToRemove});
+ const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({
+ previousEmployeeList,
+ approvalWorkflow,
+ type: CONST.APPROVAL_WORKFLOW.TYPE.UPDATE,
+ membersToRemove,
+ approversToRemove,
+ });
const optimisticData: OnyxUpdate[] = [
{
@@ -191,7 +197,7 @@ function removeApprovalWorkflow(policyID: string, approvalWorkflow: ApprovalWork
}
const previousEmployeeList = {...policy.employeeList};
- const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.REMOVE});
+ const updatedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: CONST.APPROVAL_WORKFLOW.TYPE.REMOVE});
const updatedEmployeeList = {...previousEmployeeList, ...updatedEmployees};
const defaultApprover = policy.approver ?? policy.owner;
diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts
index 8be8d2cb077a..ad3693ef3654 100644
--- a/src/libs/actions/connections/NetSuiteCommands.ts
+++ b/src/libs/actions/connections/NetSuiteCommands.ts
@@ -1,3 +1,4 @@
+import isObject from 'lodash/isObject';
import type {OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
@@ -7,6 +8,7 @@ import {WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {Connections, NetSuiteCustomFormID, NetSuiteCustomList, NetSuiteCustomSegment, NetSuiteMappingValues} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
@@ -34,6 +36,36 @@ function connectPolicyToNetSuite(policyID: string, credentials: Omit(
+ settingName: TSettingName,
+ settingValue: Partial,
+ pendingValue: OnyxCommon.PendingAction,
+) {
+ if (!isObject(settingValue)) {
+ return {[settingName]: pendingValue};
+ }
+
+ return Object.keys(settingValue).reduce>((acc, setting) => {
+ acc[setting] = pendingValue;
+ return acc;
+ }, {});
+}
+
+function createErrorFields(
+ settingName: TSettingName,
+ settingValue: Partial,
+ errorValue: OnyxCommon.Errors | null,
+) {
+ if (!isObject(settingValue)) {
+ return {[settingName]: errorValue};
+ }
+
+ return Object.keys(settingValue).reduce((acc, setting) => {
+ acc[setting] = errorValue;
+ return acc;
+ }, {});
+}
+
function updateNetSuiteOnyxData(
policyID: string,
settingName: TSettingName,
@@ -50,12 +82,8 @@ function updateNetSuiteOnyxData,
oldSettingValue: Partial,
+ modifiedFieldID?: string,
+ pendingAction?: OnyxCommon.PendingAction,
) {
+ let syncOptionsOptimisticValue;
+ if (pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ syncOptionsOptimisticValue = {
+ [settingName]: settingValue ?? null,
+ };
+ }
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -129,14 +157,12 @@ function updateNetSuiteSyncOptionsOnyxData,
- oldMappingValue?: ValueOf,
+ oldMappingValue?: ValueOf | null,
) {
const onyxData: OnyxData = {
optimisticData: [
@@ -302,14 +331,14 @@ function updateNetSuiteImportMapping
+
+
+
+
+ {translate('deeplinkWrapper.launching')}
+
+
+ {translate('deeplinkWrapper.expired')}{' '}
+ {
+ if (!NativeModules.HybridAppModule) {
+ Session.clearSignInData();
+ Navigation.navigate();
+ return;
+ }
+ NativeModules.HybridAppModule.closeReactNativeApp(true, false);
+ }}
+ >
+ {translate('deeplinkWrapper.signIn')}
+
+
+
+
+
+
+
+
+ );
+}
+
+SessionExpiredPage.displayName = 'SessionExpiredPage';
+
+export default SessionExpiredPage;
diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx
index 6eb6a6cc7161..fcbeadaa4a47 100644
--- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx
+++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx
@@ -1,17 +1,9 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useEffect} from 'react';
-import {NativeModules, View} from 'react-native';
+import {NativeModules} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-import Icon from '@components/Icon';
-import * as Expensicons from '@components/Icon/Expensicons';
-import * as Illustrations from '@components/Icon/Illustrations';
-import Text from '@components/Text';
-import TextLink from '@components/TextLink';
-import useLocalize from '@hooks/useLocalize';
-import useTheme from '@hooks/useTheme';
-import useThemeStyles from '@hooks/useThemeStyles';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import type {PublicScreensParamList} from '@libs/Navigation/types';
@@ -22,6 +14,7 @@ import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {Account} from '@src/types/onyx';
+import SessionExpiredPage from './ErrorPage/SessionExpiredPage';
type LogInWithShortLivedAuthTokenPageOnyxProps = {
/** The details about the account that the user is signing in with */
@@ -31,9 +24,6 @@ type LogInWithShortLivedAuthTokenPageOnyxProps = {
type LogInWithShortLivedAuthTokenPageProps = LogInWithShortLivedAuthTokenPageOnyxProps & StackScreenProps;
function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedAuthTokenPageProps) {
- const theme = useTheme();
- const styles = useThemeStyles();
- const {translate} = useLocalize();
const {email = '', shortLivedAuthToken = '', shortLivedToken = '', authTokenType, exitTo, error} = route?.params ?? {};
useEffect(() => {
@@ -76,41 +66,7 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA
return ;
}
- return (
-
-
-
-
-
- {translate('deeplinkWrapper.launching')}
-
-
- {translate('deeplinkWrapper.expired')}{' '}
- {
- Session.clearSignInData();
- Navigation.navigate();
- }}
- >
- {translate('deeplinkWrapper.signIn')}
-
-
-
-
-
-
-
-
- );
+ return ;
}
LogInWithShortLivedAuthTokenPage.displayName = 'LogInWithShortLivedAuthTokenPage';
diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx
index f5b96e2d57c5..deb95a576c3d 100644
--- a/src/pages/LogOutPreviousUserPage.tsx
+++ b/src/pages/LogOutPreviousUserPage.tsx
@@ -31,7 +31,7 @@ type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreen
//
// This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate
function LogOutPreviousUserPage({session, route, isAccountLoading}: LogOutPreviousUserPageProps) {
- const initialURL = useContext(InitialURLContext);
+ const {initialURL} = useContext(InitialURLContext);
useEffect(() => {
const sessionEmail = session?.email;
diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx
index 230e81fbd859..eeacd56f8d5c 100755
--- a/src/pages/settings/InitialSettingsPage.tsx
+++ b/src/pages/settings/InitialSettingsPage.tsx
@@ -11,6 +11,7 @@ import AccountSwitcherSkeletonView from '@components/AccountSwitcherSkeletonView
import ConfirmModal from '@components/ConfirmModal';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
+import {InitialURLContext} from '@components/InitialURLContextProvider';
import MenuItem from '@components/MenuItem';
import {PressableWithFeedback} from '@components/Pressable';
import ScreenWrapper from '@components/ScreenWrapper';
@@ -104,6 +105,7 @@ function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms
const activeCentralPaneRoute = useActiveCentralPaneRoute();
const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? '';
const [allConnectionSyncProgresses] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}`);
+ const {setInitialURL} = useContext(InitialURLContext);
const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION);
const subscriptionPlan = useSubscriptionPlan();
@@ -149,6 +151,7 @@ function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms
? {
action: () => {
NativeModules.HybridAppModule.closeReactNativeApp(false, true);
+ setInitialURL(undefined);
},
}
: {
@@ -184,7 +187,7 @@ function InitialSettingsPage({userWallet, bankAccountList, fundList, walletTerms
};
return defaultMenu;
- }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors]);
+ }, [loginList, fundList, styles.accountSettingsSectionContainer, bankAccountList, userWallet?.errors, walletTerms?.errors, setInitialURL]);
/**
* Retuns a list of menu items data for workspace section
diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx
index 3aab8a003bc9..de9260dac537 100644
--- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx
+++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.tsx
@@ -4,10 +4,10 @@ import type {ImageSourcePropType} from 'react-native';
import Reanimated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import DesktopBackgroundImage from '@assets/images/home-background--desktop.svg';
import MobileBackgroundImage from '@assets/images/home-background--mobile-new.svg';
-import useSplashScreen from '@hooks/useSplashScreen';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
+import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
import type BackgroundImageProps from './types';
function BackgroundImage({width, transitionDuration, isSmallScreen = false}: BackgroundImageProps) {
@@ -26,10 +26,10 @@ function BackgroundImage({width, transitionDuration, isSmallScreen = false}: Bac
});
}
- const {isSplashHidden} = useSplashScreen();
+ const {splashScreenState} = useSplashScreenStateContext();
// Prevent rendering the background image until the splash screen is hidden.
// See issue: https://github.com/Expensify/App/issues/34696
- if (!isSplashHidden) {
+ if (splashScreenState !== CONST.BOOT_SPLASH_STATE.HIDDEN) {
return;
}
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index 8a54d8e6c9f5..c727c2cf3496 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -51,6 +51,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const styles = useThemeStyles();
const {translate, datetimeToRelative: getDatetimeToRelative} = useLocalize();
const {isOffline} = useNetwork();
+ const {canUseNetSuiteUSATax} = usePermissions();
const {windowWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0});
@@ -227,7 +228,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
}
const shouldShowSynchronizationError = !!synchronizationError;
const shouldHideConfigurationOptions = isConnectionUnverified(policy, connectedIntegration);
- const integrationData = getAccountingIntegrationData(connectedIntegration, policyID, translate, policy);
+ const integrationData = getAccountingIntegrationData(connectedIntegration, policyID, translate, policy, undefined, canUseNetSuiteUSATax);
const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {};
const configurationOptions = [
@@ -338,6 +339,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
isOffline,
startIntegrationFlow,
popoverAnchorRefs,
+ canUseNetSuiteUSATax,
]);
const otherIntegrationsItems = useMemo(() => {
@@ -425,6 +427,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx
index ed70da6f1608..b475966f6f88 100644
--- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteExpenseReportApprovalLevelSelectPage.tsx
@@ -9,9 +9,12 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import {settingsPendingAction} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -65,6 +68,10 @@ function NetSuiteExpenseReportApprovalLevelSelectPage({policy}: WithPolicyConnec
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID))}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
shouldBeBlocked={config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_REPORTS_TO], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_REPORTS_TO)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_REPORTS_TO)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx
index ee1d1108ffd6..dae2db4d191a 100644
--- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteJournalEntryApprovalLevelSelectPage.tsx
@@ -9,9 +9,12 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import {settingsPendingAction} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -68,6 +71,10 @@ function NetSuiteJournalEntryApprovalLevelSelectPage({policy}: WithPolicyConnect
config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY &&
config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY
}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_JOURNALS_TO], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_JOURNALS_TO)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_JOURNALS_TO)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage.tsx
index 4ad1a1b62633..01fbfc8d437c 100644
--- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteReimbursementAccountSelectPage.tsx
@@ -9,11 +9,13 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {getNetSuiteReimbursableAccountOptions} from '@libs/PolicyUtils';
+import {getNetSuiteReimbursableAccountOptions, settingsPendingAction} from '@libs/PolicyUtils';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import variables from '@styles/variables';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -80,6 +82,10 @@ function NetSuiteReimbursementAccountSelectPage({policy}: WithPolicyConnectionsP
listEmptyContent={listEmptyContent}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
shouldBeBlocked={config?.reimbursableExpensesExportDestination === CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.REIMBURSEMENT_ACCOUNT_ID], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.REIMBURSEMENT_ACCOUNT_ID)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.REIMBURSEMENT_ACCOUNT_ID)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx
index ecbaacd46bad..5a1cfe0797eb 100644
--- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteVendorBillApprovalLevelSelectPage.tsx
@@ -9,9 +9,12 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import {settingsPendingAction} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -72,6 +75,10 @@ function NetSuiteVendorBillApprovalLevelSelectPage({policy}: WithPolicyConnectio
config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL &&
config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL
}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_VENDOR_BILLS_TO], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_VENDOR_BILLS_TO)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.EXPORT_VENDOR_BILLS_TO)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx
index 42d014ed4299..ce65db521257 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteDateSelectPage.tsx
@@ -9,9 +9,12 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import {settingsPendingAction} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -66,6 +69,10 @@ function NetSuiteDateSelectPage({policy}: WithPolicyConnectionsProps) {
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.EXPORT_DATE], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORT_DATE)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORT_DATE)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx
index 648ec7d77a64..88e7934bf17f 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportConfigurationPage.tsx
@@ -10,13 +10,23 @@ import * as Connections from '@libs/actions/connections/NetSuiteCommands';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import {
- canUseProvincialTaxNetSuite,
- canUseTaxNetSuite,
+ areSettingsInErrorFields,
findSelectedBankAccountWithDefaultSelect,
findSelectedInvoiceItemWithDefaultSelect,
findSelectedTaxAccountWithDefaultSelect,
+ settingsPendingAction,
} from '@libs/PolicyUtils';
import type {DividerLineItem, MenuItem, ToggleItem} from '@pages/workspace/accounting/netsuite/types';
+import {
+ shouldHideExportForeignCurrencyAmount,
+ shouldHideJournalPostingPreference,
+ shouldHideNonReimbursableJournalPostingAccount,
+ shouldHideProvincialTaxPostingAccountSelect,
+ shouldHideReimbursableDefaultVendor,
+ shouldHideReimbursableJournalPostingAccount,
+ shouldHideTaxPostingAccountSelect,
+ shouldShowInvoiceItemMenuItem,
+} from '@pages/workspace/accounting/netsuite/utils';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
@@ -24,6 +34,8 @@ import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
+type MenuItemWithSubscribedSettings = Pick & {subscribedSettings?: string[]};
+
function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
@@ -60,18 +72,15 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
[taxAccountsList, config?.provincialTaxPostingAccount],
);
- const menuItems: Array = [
+ const menuItems: Array = [
{
type: 'menuitem',
+ title: config?.exporter ?? policyOwner,
description: translate('workspace.accounting.preferredExporter'),
onPress: () => {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_PREFERRED_EXPORTER_SELECT.getRoute(policyID));
},
- brickRoadIndicator: config?.errorFields?.exporter ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: config?.exporter ?? policyOwner,
- pendingAction: config?.pendingFields?.exporter,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORTER),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORTER),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.EXPORTER],
},
{
type: 'divider',
@@ -79,37 +88,40 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
},
{
type: 'menuitem',
- description: translate('workspace.accounting.exportDate'),
- onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_DATE_SELECT.getRoute(policyID)),
- brickRoadIndicator: config?.errorFields?.exportDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
title: config?.exportDate
? translate(`workspace.netsuite.exportDate.values.${config.exportDate}.label`)
: translate(`workspace.netsuite.exportDate.values.${CONST.NETSUITE_EXPORT_DATE.LAST_EXPENSE}.label`),
- pendingAction: config?.pendingFields?.exportDate,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORT_DATE),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORT_DATE),
+ description: translate('workspace.accounting.exportDate'),
+ onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_DATE_SELECT.getRoute(policyID)),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.EXPORT_DATE],
},
{
type: 'menuitem',
+ title: config?.reimbursableExpensesExportDestination ? translate(`workspace.netsuite.exportDestination.values.${config.reimbursableExpensesExportDestination}.label`) : undefined,
description: translate('workspace.accounting.exportOutOfPocket'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE)),
- brickRoadIndicator: config?.errorFields?.reimbursableExpensesExportDestination ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: config?.reimbursableExpensesExportDestination ? translate(`workspace.netsuite.exportDestination.values.${config.reimbursableExpensesExportDestination}.label`) : undefined,
- pendingAction: config?.pendingFields?.reimbursableExpensesExportDestination,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION),
+ subscribedSettings: [
+ CONST.NETSUITE_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION,
+ ...(!shouldHideReimbursableDefaultVendor(true, config) ? [CONST.NETSUITE_CONFIG.DEFAULT_VENDOR] : []),
+ ...(!shouldHideNonReimbursableJournalPostingAccount(true, config) ? [CONST.NETSUITE_CONFIG.PAYABLE_ACCT] : []),
+ ...(!shouldHideReimbursableJournalPostingAccount(true, config) ? [CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT] : []),
+ ...(!shouldHideJournalPostingPreference(true, config) ? [CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE] : []),
+ ],
},
{
type: 'menuitem',
- description: translate('workspace.accounting.exportCompanyCard'),
- onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE)),
- brickRoadIndicator: config?.errorFields?.nonreimbursableExpensesExportDestination ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
title: config?.nonreimbursableExpensesExportDestination
? translate(`workspace.netsuite.exportDestination.values.${config.nonreimbursableExpensesExportDestination}.label`)
: undefined,
- pendingAction: config?.pendingFields?.nonreimbursableExpensesExportDestination,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION),
+ description: translate('workspace.accounting.exportCompanyCard'),
+ onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE)),
+ subscribedSettings: [
+ CONST.NETSUITE_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION,
+ ...(!shouldHideReimbursableDefaultVendor(false, config) ? [CONST.NETSUITE_CONFIG.DEFAULT_VENDOR] : []),
+ ...(!shouldHideNonReimbursableJournalPostingAccount(false, config) ? [CONST.NETSUITE_CONFIG.PAYABLE_ACCT] : []),
+ ...(!shouldHideReimbursableJournalPostingAccount(false, config) ? [CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT] : []),
+ ...(!shouldHideJournalPostingPreference(false, config) ? [CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE] : []),
+ ],
},
{
type: 'divider',
@@ -117,23 +129,17 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
},
{
type: 'menuitem',
+ title: selectedReceivable ? selectedReceivable.name : undefined,
description: translate('workspace.netsuite.exportInvoices'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_RECEIVABLE_ACCOUNT_SELECT.getRoute(policyID)),
- brickRoadIndicator: config?.errorFields?.receivableAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: selectedReceivable ? selectedReceivable.name : undefined,
- pendingAction: config?.pendingFields?.receivableAccount,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT],
},
{
type: 'menuitem',
+ title: invoiceItemValue,
description: translate('workspace.netsuite.invoiceItem.label'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_PREFERENCE_SELECT.getRoute(policyID)),
- brickRoadIndicator: config?.errorFields?.invoiceItemPreference ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: invoiceItemValue,
- pendingAction: config?.pendingFields?.invoiceItemPreference,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE, ...(shouldShowInvoiceItemMenuItem(config) ? [CONST.NETSUITE_CONFIG.INVOICE_ITEM] : [])],
},
{
type: 'divider',
@@ -141,25 +147,19 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
},
{
type: 'menuitem',
+ title: selectedProvTaxPostingAccount ? selectedProvTaxPostingAccount.name : undefined,
description: translate('workspace.netsuite.journalEntriesProvTaxPostingAccount'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_PROVINCIAL_TAX_POSTING_ACCOUNT_SELECT.getRoute(policyID)),
- brickRoadIndicator: config?.errorFields?.provincialTaxPostingAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: selectedProvTaxPostingAccount ? selectedProvTaxPostingAccount.name : undefined,
- pendingAction: config?.pendingFields?.provincialTaxPostingAccount,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT),
- shouldHide: !!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseProvincialTaxNetSuite(selectedSubsidiary?.country),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT],
+ shouldHide: shouldHideProvincialTaxPostingAccountSelect(selectedSubsidiary, config),
},
{
type: 'menuitem',
+ title: selectedTaxPostingAccount ? selectedTaxPostingAccount.name : undefined,
description: translate('workspace.netsuite.journalEntriesTaxPostingAccount'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_TAX_POSTING_ACCOUNT_SELECT.getRoute(policyID)),
- brickRoadIndicator: config?.errorFields?.taxPostingAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
- title: selectedTaxPostingAccount ? selectedTaxPostingAccount.name : undefined,
- pendingAction: config?.pendingFields?.taxPostingAccount,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT),
- shouldHide: !!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseTaxNetSuite(canUseNetSuiteUSATax, selectedSubsidiary?.country),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT],
+ shouldHide: shouldHideTaxPostingAccountSelect(canUseNetSuiteUSATax, selectedSubsidiary, config),
},
{
type: 'toggle',
@@ -168,11 +168,9 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
switchAccessibilityLabel: translate('workspace.netsuite.foreignCurrencyAmount'),
onToggle: () => Connections.updateNetSuiteAllowForeignCurrency(policyID, !config?.allowForeignCurrency, config?.allowForeignCurrency),
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY),
- pendingAction: config?.pendingFields?.allowForeignCurrency,
+ pendingAction: settingsPendingAction([CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY], config?.pendingFields),
errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.ALLOW_FOREIGN_CURRENCY),
- shouldHide:
- config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT &&
- config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.EXPENSE_REPORT,
+ shouldHide: shouldHideExportForeignCurrencyAmount(config),
},
{
type: 'toggle',
@@ -181,7 +179,7 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
switchAccessibilityLabel: translate('workspace.netsuite.exportToNextOpenPeriod'),
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD),
onToggle: () => Connections.updateNetSuiteExportToNextOpenPeriod(policyID, !config?.exportToNextOpenPeriod, config?.exportToNextOpenPeriod ?? false),
- pendingAction: config?.pendingFields?.exportToNextOpenPeriod,
+ pendingAction: settingsPendingAction([CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD], config?.pendingFields),
errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORT_TO_NEXT_OPEN_PERIOD),
},
];
@@ -225,19 +223,14 @@ function NetSuiteExportConfigurationPage({policy}: WithPolicyConnectionsProps) {
return (
);
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx
index 535951a51f87..403f6b6a192d 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesDestinationSelectPage.tsx
@@ -6,11 +6,16 @@ import type {ListItem} from '@components/SelectionList/types';
import SelectionScreen from '@components/SelectionScreen';
import type {SelectorType} from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import {settingsPendingAction} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types';
+import {exportExpensesDestinationSettingName} from '@pages/workspace/accounting/netsuite/utils';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -20,6 +25,7 @@ type MenuListItem = ListItem & {
function NetSuiteExportExpensesDestinationSelectPage({policy}: WithPolicyConnectionsProps) {
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const policyID = policy?.id ?? '-1';
const config = policy?.connections?.netsuite.options.config;
@@ -27,7 +33,8 @@ function NetSuiteExportExpensesDestinationSelectPage({policy}: WithPolicyConnect
const params = route.params as ExpenseRouteParams;
const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE;
- const currentDestination = isReimbursable ? config?.reimbursableExpensesExportDestination : config?.nonreimbursableExpensesExportDestination;
+ const currentSettingName = exportExpensesDestinationSettingName(isReimbursable);
+ const currentDestination = config?.[currentSettingName];
const data: MenuListItem[] = Object.values(CONST.NETSUITE_EXPORT_DESTINATION).map((dateType) => ({
value: dateType,
@@ -63,6 +70,10 @@ function NetSuiteExportExpensesDestinationSelectPage({policy}: WithPolicyConnect
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES.getRoute(policyID, params.expenseType))}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
+ pendingAction={settingsPendingAction([currentSettingName], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, currentSettingName)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, currentSettingName)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx
index 544cd0cc20cd..f381bdfec3be 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesJournalPostingPreferenceSelectPage.tsx
@@ -6,11 +6,15 @@ import type {ListItem} from '@components/SelectionList/types';
import SelectionScreen from '@components/SelectionScreen';
import type {SelectorType} from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import {settingsPendingAction} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -20,6 +24,7 @@ type MenuListItem = ListItem & {
function NetSuiteExportExpensesJournalPostingPreferenceSelectPage({policy}: WithPolicyConnectionsProps) {
const {translate} = useLocalize();
+ const styles = useThemeStyles();
const policyID = policy?.id ?? '-1';
const config = policy?.connections?.netsuite.options.config;
@@ -66,6 +71,10 @@ function NetSuiteExportExpensesJournalPostingPreferenceSelectPage({policy}: With
? config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY
: config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY
}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx
index c3ffe000e1e5..ad92c53acad1 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPage.tsx
@@ -5,17 +5,25 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {findSelectedBankAccountWithDefaultSelect, findSelectedVendorWithDefaultSelect} from '@libs/PolicyUtils';
+import {areSettingsInErrorFields, findSelectedBankAccountWithDefaultSelect, findSelectedVendorWithDefaultSelect, settingsPendingAction} from '@libs/PolicyUtils';
import type {ExpenseRouteParams, MenuItem} from '@pages/workspace/accounting/netsuite/types';
+import {
+ exportExpensesDestinationSettingName,
+ shouldHideJournalPostingPreference,
+ shouldHideNonReimbursableJournalPostingAccount,
+ shouldHideReimbursableDefaultVendor,
+ shouldHideReimbursableJournalPostingAccount,
+} from '@pages/workspace/accounting/netsuite/utils';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
-type MenuItemWithoutType = Omit;
+type MenuItemWithSubscribedSettings = Pick & {
+ subscribedSettings?: string[];
+};
function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) {
const {translate} = useLocalize();
@@ -27,11 +35,9 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) {
const config = policy?.connections?.netsuite?.options.config;
- const exportDestination = isReimbursable ? config?.reimbursableExpensesExportDestination : config?.nonreimbursableExpensesExportDestination;
- const exportDestinationError = isReimbursable ? config?.errorFields?.reimbursableExpensesExportDestination : config?.errorFields?.nonreimbursableExpensesExportDestination;
- const exportDestinationPending = isReimbursable ? config?.pendingFields?.reimbursableExpensesExportDestination : config?.pendingFields?.nonreimbursableExpensesExportDestination;
+ const exportDestinationSettingName = exportExpensesDestinationSettingName(isReimbursable);
+ const exportDestination = config?.[exportDestinationSettingName];
const helperTextType = isReimbursable ? 'reimbursableDescription' : 'nonReimbursableDescription';
- const configType = isReimbursable ? CONST.NETSUITE_CONFIG.REIMBURSABLE_EXPENSES_EXPORT_DESTINATION : CONST.NETSUITE_CONFIG.NON_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION;
const {vendors, payableList} = policy?.connections?.netsuite?.options?.data ?? {};
@@ -44,59 +50,49 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) {
[payableList, config?.reimbursablePayableAccount],
);
- const menuItems: MenuItemWithoutType[] = [
+ const menuItems: MenuItemWithSubscribedSettings[] = [
{
description: translate('workspace.accounting.exportAs'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_DESTINATION_SELECT.getRoute(policyID, params.expenseType)),
- brickRoadIndicator: exportDestinationError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
title: exportDestination ? translate(`workspace.netsuite.exportDestination.values.${exportDestination}.label`) : undefined,
- pendingAction: exportDestinationPending,
- errors: ErrorUtils.getLatestErrorField(config, configType),
- onCloseError: () => Policy.clearNetSuiteErrorField(policyID, configType),
+ subscribedSettings: [exportDestinationSettingName],
+ onCloseError: () => Policy.clearNetSuiteErrorField(policyID, exportDestinationSettingName),
helperText: exportDestination ? translate(`workspace.netsuite.exportDestination.values.${exportDestination}.${helperTextType}`) : undefined,
shouldParseHelperText: true,
},
{
description: translate('workspace.accounting.defaultVendor'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_VENDOR_SELECT.getRoute(policyID, params.expenseType)),
- brickRoadIndicator: config?.errorFields?.defaultVendor ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
title: defaultVendor ? defaultVendor.name : undefined,
- pendingAction: config?.pendingFields?.defaultVendor,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.DEFAULT_VENDOR),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.DEFAULT_VENDOR],
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.DEFAULT_VENDOR),
- shouldHide: isReimbursable || exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL,
+ shouldHide: shouldHideReimbursableDefaultVendor(isReimbursable, config),
},
{
description: translate('workspace.netsuite.nonReimbursableJournalPostingAccount'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT.getRoute(policyID, params.expenseType)),
- brickRoadIndicator: config?.errorFields?.payableAcct ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
title: selectedPayableAccount ? selectedPayableAccount.name : undefined,
- pendingAction: config?.pendingFields?.payableAcct,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.PAYABLE_ACCT),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.PAYABLE_ACCT],
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.PAYABLE_ACCT),
- shouldHide: isReimbursable || exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY,
+ shouldHide: shouldHideNonReimbursableJournalPostingAccount(isReimbursable, config),
},
{
description: translate('workspace.netsuite.reimbursableJournalPostingAccount'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_PAYABLE_ACCOUNT_SELECT.getRoute(policyID, params.expenseType)),
- brickRoadIndicator: config?.errorFields?.reimbursablePayableAccount ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
title: selectedReimbursablePayableAccount ? selectedReimbursablePayableAccount.name : undefined,
- pendingAction: config?.pendingFields?.reimbursablePayableAccount,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT],
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT),
- shouldHide: !isReimbursable || exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY,
+ shouldHide: shouldHideReimbursableJournalPostingAccount(isReimbursable, config),
},
{
description: translate('workspace.netsuite.journalPostingPreference.label'),
onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT_EXPENSES_JOURNAL_POSTING_PREFERENCE_SELECT.getRoute(policyID, params.expenseType)),
- brickRoadIndicator: config?.errorFields?.journalPostingPreference ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
title: config?.journalPostingPreference
? translate(`workspace.netsuite.journalPostingPreference.values.${config.journalPostingPreference}`)
: translate(`workspace.netsuite.journalPostingPreference.values.${CONST.NETSUITE_JOURNAL_POSTING_PREFERENCE.JOURNALS_POSTING_INDIVIDUAL_LINE}`),
- pendingAction: config?.pendingFields?.journalPostingPreference,
- errors: ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE),
+ subscribedSettings: [CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE],
onCloseError: () => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.JOURNAL_POSTING_PREFERENCE),
- shouldHide: exportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY,
+ shouldHide: shouldHideJournalPostingPreference(isReimbursable, config),
},
];
@@ -117,19 +113,15 @@ function NetSuiteExportExpensesPage({policy}: WithPolicyConnectionsProps) {
.map((item) => (
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx
index 0461171920d4..97e470776281 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesPayableAccountSelectPage.tsx
@@ -8,12 +8,14 @@ import SelectionScreen from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {getNetSuitePayableAccountOptions} from '@libs/PolicyUtils';
+import {getNetSuitePayableAccountOptions, settingsPendingAction} from '@libs/PolicyUtils';
import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import variables from '@styles/variables';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -28,7 +30,8 @@ function NetSuiteExportExpensesPayableAccountSelectPage({policy}: WithPolicyConn
const isReimbursable = params.expenseType === CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE;
const config = policy?.connections?.netsuite.options.config;
- const currentPayableAccountID = isReimbursable ? config?.reimbursablePayableAccount : config?.payableAcct;
+ const currentSettingName = isReimbursable ? CONST.NETSUITE_CONFIG.REIMBURSABLE_PAYABLE_ACCOUNT : CONST.NETSUITE_CONFIG.PAYABLE_ACCT;
+ const currentPayableAccountID = config?.[currentSettingName];
const netsuitePayableAccountOptions = useMemo(() => getNetSuitePayableAccountOptions(policy ?? undefined, currentPayableAccountID), [currentPayableAccountID, policy]);
const initiallyFocusedOptionKey = useMemo(() => netsuitePayableAccountOptions?.find((mode) => mode.isSelected)?.keyForList, [netsuitePayableAccountOptions]);
@@ -80,6 +83,10 @@ function NetSuiteExportExpensesPayableAccountSelectPage({policy}: WithPolicyConn
? config?.reimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY
: config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY
}
+ pendingAction={settingsPendingAction([currentSettingName], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, currentSettingName)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, currentSettingName)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx
index b6034a29a949..6f4cfe9817c3 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteExportExpensesVendorSelectPage.tsx
@@ -8,12 +8,14 @@ import SelectionScreen from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {getNetSuiteVendorOptions} from '@libs/PolicyUtils';
+import {getNetSuiteVendorOptions, settingsPendingAction} from '@libs/PolicyUtils';
import type {ExpenseRouteParams} from '@pages/workspace/accounting/netsuite/types';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import variables from '@styles/variables';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -71,6 +73,10 @@ function NetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsP
listEmptyContent={listEmptyContent}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
shouldBeBlocked={isReimbursable || config?.nonreimbursableExpensesExportDestination !== CONST.NETSUITE_EXPORT_DESTINATION.VENDOR_BILL}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.DEFAULT_VENDOR], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.DEFAULT_VENDOR)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.DEFAULT_VENDOR)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx
index 73a452c81745..f07edd7bd610 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemPreferenceSelectPage.tsx
@@ -1,18 +1,18 @@
import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
+import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import type {ListItem} from '@components/SelectionList/types';
-import SelectionScreen from '@components/SelectionScreen';
import type {SelectorType} from '@components/SelectionScreen';
-import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
import * as ErrorUtils from '@libs/ErrorUtils';
-import {findSelectedInvoiceItemWithDefaultSelect} from '@libs/PolicyUtils';
+import {areSettingsInErrorFields, findSelectedInvoiceItemWithDefaultSelect, settingsPendingAction} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
@@ -54,40 +54,43 @@ function NetSuiteInvoiceItemPreferenceSelectPage({policy}: WithPolicyConnections
[config?.invoiceItemPreference, policyID],
);
- const headerContent = useMemo(
- () => (
-
-
- {translate(`workspace.netsuite.invoiceItem.values.${config?.invoiceItemPreference ?? CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.CREATE}.description`)}
-
-
- ),
- [styles.pb2, styles.ph5, styles.textNormal, translate, config?.invoiceItemPreference],
- );
-
return (
- selectInvoicePreference(selection as MenuListItem)}
- initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList}
- policyID={policyID}
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))}
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
- onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))}
+ displayName={NetSuiteInvoiceItemPreferenceSelectPage.displayName}
+ policyID={policyID}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
- shouldUpdateFocusedIndex
- listFooterContent={
- config?.invoiceItemPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT ? (
+ shouldUseScrollView={false}
+ shouldIncludeSafeAreaPaddingBottom
+ >
+ Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM_PREFERENCE)}
+ style={[styles.flexGrow1, styles.flexShrink1]}
+ contentContainerStyle={[styles.flexGrow1, styles.flexShrink1]}
+ >
+ selectInvoicePreference(selection as MenuListItem)}
+ sections={[{data}]}
+ ListItem={RadioListItem}
+ showScrollIndicator
+ shouldUpdateFocusedIndex
+ initiallyFocusedOptionKey={data.find((mode) => mode.isSelected)?.keyForList}
+ containerStyle={[styles.flexReset, styles.flexGrow1, styles.flexShrink1, styles.pb0]}
+ />
+
+ {config?.invoiceItemPreference === CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT && (
+
Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM)}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.INVOICE_ITEM], config?.pendingFields)}
>
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_INVOICE_ITEM_SELECT.getRoute(policyID))}
- brickRoadIndicator={config?.errorFields?.invoiceItem ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ brickRoadIndicator={areSettingsInErrorFields([CONST.NETSUITE_CONFIG.INVOICE_ITEM], config?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
- ) : null
- }
- />
+
+ )}
+
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx
index 26af047a3ef4..cfe0cdc89f93 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteInvoiceItemSelectPage.tsx
@@ -7,11 +7,13 @@ import SelectionScreen from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {getNetSuiteInvoiceItemOptions} from '@libs/PolicyUtils';
+import {getNetSuiteInvoiceItemOptions, settingsPendingAction} from '@libs/PolicyUtils';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import variables from '@styles/variables';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -65,6 +67,10 @@ function NetSuiteInvoiceItemSelectPage({policy}: WithPolicyConnectionsProps) {
listEmptyContent={listEmptyContent}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
shouldBeBlocked={config?.invoiceItemPreference !== CONST.NETSUITE_INVOICE_ITEM_PREFERENCE.SELECT}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.INVOICE_ITEM], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.INVOICE_ITEM)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.INVOICE_ITEM)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx
index 92d9a76bf9e4..b07a717a7abb 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuitePreferredExporterSelectPage.tsx
@@ -9,10 +9,12 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
-import {getAdminEmployees, isExpensifyTeam} from '@libs/PolicyUtils';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import {getAdminEmployees, isExpensifyTeam, settingsPendingAction} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -95,6 +97,10 @@ function NetSuitePreferredExporterSelectPage({policy}: WithPolicyConnectionsProp
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID))}
title="workspace.accounting.preferredExporter"
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.EXPORTER], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.EXPORTER)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.EXPORTER)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx
index 71a10432d1ed..b0edc94cede9 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteProvincialTaxPostingAccountSelectPage.tsx
@@ -7,11 +7,13 @@ import SelectionScreen from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {canUseProvincialTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/PolicyUtils';
+import {canUseProvincialTaxNetSuite, getNetSuiteTaxAccountOptions, settingsPendingAction} from '@libs/PolicyUtils';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import variables from '@styles/variables';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -71,6 +73,10 @@ function NetSuiteProvincialTaxPostingAccountSelectPage({policy}: WithPolicyConne
listEmptyContent={listEmptyContent}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
shouldBeBlocked={!!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseProvincialTaxNetSuite(selectedSubsidiary?.country)}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.PROVINCIAL_TAX_POSTING_ACCOUNT)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx
index 4c1e62d66674..2bbd2fe37dc8 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteReceivableAccountSelectPage.tsx
@@ -7,11 +7,13 @@ import SelectionScreen from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {getNetSuiteReceivableAccountOptions} from '@libs/PolicyUtils';
+import {getNetSuiteReceivableAccountOptions, settingsPendingAction} from '@libs/PolicyUtils';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import variables from '@styles/variables';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -67,6 +69,10 @@ function NetSuiteReceivableAccountSelectPage({policy}: WithPolicyConnectionsProp
title="workspace.netsuite.exportInvoices"
listEmptyContent={listEmptyContent}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.RECEIVABLE_ACCOUNT)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx
index f7b7295e3a3a..5d749a433606 100644
--- a/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/export/NetSuiteTaxPostingAccountSelectPage.tsx
@@ -8,11 +8,13 @@ import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections/NetSuiteCommands';
+import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import {canUseTaxNetSuite, getNetSuiteTaxAccountOptions} from '@libs/PolicyUtils';
+import {canUseTaxNetSuite, getNetSuiteTaxAccountOptions, settingsPendingAction} from '@libs/PolicyUtils';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import variables from '@styles/variables';
+import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -73,6 +75,10 @@ function NetSuiteTaxPostingAccountSelectPage({policy}: WithPolicyConnectionsProp
listEmptyContent={listEmptyContent}
connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE}
shouldBeBlocked={!!config?.suiteTaxEnabled || !config?.syncOptions.syncTax || !canUseTaxNetSuite(canUseNetSuiteUSATax, selectedSubsidiary?.country)}
+ pendingAction={settingsPendingAction([CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT], config?.pendingFields)}
+ errors={ErrorUtils.getLatestErrorField(config, CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT)}
+ errorRowStyles={[styles.ph5, styles.pv3]}
+ onClose={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.TAX_POSTING_ACCOUNT)}
/>
);
}
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit.tsx
index d3f5082d47cb..9faea6197215 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldEdit.tsx
@@ -12,6 +12,7 @@ import {updateNetSuiteCustomLists, updateNetSuiteCustomSegments} from '@libs/act
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
+import {settingsPendingAction} from '@libs/PolicyUtils';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import CONST from '@src/CONST';
@@ -72,9 +73,21 @@ function NetSuiteImportCustomFieldEdit({
});
if (PolicyUtils.isNetSuiteCustomSegmentRecord(customField)) {
- updateNetSuiteCustomSegments(policyID, updatedRecords as NetSuiteCustomSegment[], allRecords as NetSuiteCustomSegment[]);
+ updateNetSuiteCustomSegments(
+ policyID,
+ updatedRecords as NetSuiteCustomSegment[],
+ allRecords as NetSuiteCustomSegment[],
+ `${importCustomField}_${valueIndex}`,
+ CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ );
} else {
- updateNetSuiteCustomLists(policyID, updatedRecords as NetSuiteCustomList[], allRecords as NetSuiteCustomList[]);
+ updateNetSuiteCustomLists(
+ policyID,
+ updatedRecords as NetSuiteCustomList[],
+ allRecords as NetSuiteCustomList[],
+ `${importCustomField}_${valueIndex}`,
+ CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ );
}
}
@@ -115,7 +128,7 @@ function NetSuiteImportCustomFieldEdit({
submitButtonText={translate('common.save')}
shouldValidateOnBlur
shouldValidateOnChange
- isSubmitDisabled={!!config?.syncOptions?.pendingFields?.[importCustomField]}
+ isSubmitDisabled={!!settingsPendingAction([`${importCustomField}_${valueIndex}`], config?.pendingFields)}
>
),
- [config?.syncOptions?.pendingFields, customField, fieldName, fieldValue, importCustomField, inputCallbackRef, styles.flexGrow1, styles.ph5, translate, updateRecord, validate],
+ [config?.pendingFields, customField, fieldName, fieldValue, importCustomField, inputCallbackRef, styles.flexGrow1, styles.ph5, translate, updateRecord, validate, valueIndex],
);
const renderSelection = useMemo(
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx
index e08cf4dac6c4..794ecc4b118a 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteImportAddCustomListPage.tsx
@@ -110,7 +110,13 @@ function NetSuiteImportAddCustomListPage({policy}: WithPolicyConnectionsProps) {
mapping: formValues[INPUT_IDS.MAPPING] ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG,
},
]);
- Connections.updateNetSuiteCustomLists(policyID, updatedCustomLists, customLists);
+ Connections.updateNetSuiteCustomLists(
+ policyID,
+ updatedCustomLists,
+ customLists,
+ `${CONST.NETSUITE_CONFIG.IMPORT_CUSTOM_FIELDS.CUSTOM_LISTS}_${customLists.length}`,
+ CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ );
nextScreen();
},
[customLists, nextScreen, policyID],
@@ -153,7 +159,7 @@ function NetSuiteImportAddCustomListPage({policy}: WithPolicyConnectionsProps) {
submitButtonStyles={[styles.ph5, styles.mb0]}
shouldUseScrollView={!selectionListForm}
enabledWhenOffline
- isSubmitDisabled={!!config?.syncOptions?.pendingFields?.customLists}
+ isSubmitDisabled={!!config?.pendingFields?.customLists}
submitFlexEnabled={submitFlexAllowed}
>
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldPage.tsx
index 0a749b01399f..fa979cd7454c 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldPage.tsx
@@ -14,12 +14,11 @@ import TextLink from '@components/TextLink';
import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
+import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import type {ThemeStyles} from '@styles/index';
-import * as Policy from '@userActions/Policy/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
@@ -123,29 +122,27 @@ function NetSuiteImportCustomFieldPage({
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT.getRoute(policyID))}
>
{data.length === 0 ? listEmptyComponent : listHeaderComponent}
- Policy.clearNetSuiteErrorField(policyID, importCustomField)}
- >
- {data.map((record, index) => (
+ {data.map((record, index) => (
+
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOM_FIELD_VIEW.getRoute(policyID, importCustomField, index))}
+ brickRoadIndicator={areSettingsInErrorFields([`${importCustomField}_${index}`], config?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
/>
- ))}
-
+
+ ))}