diff --git a/assets/images/integrationicons/netsuite-quickstart-icon-square.svg b/assets/images/integrationicons/netsuite-quickstart-icon-square.svg
new file mode 100644
index 0000000000000..5b8ddb542cf77
--- /dev/null
+++ b/assets/images/integrationicons/netsuite-quickstart-icon-square.svg
@@ -0,0 +1,35 @@
+
+
\ No newline at end of file
diff --git a/src/CONST.ts b/src/CONST.ts
index 1250092cb9104..19bc28d61d51b 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1,6 +1,9 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {add as dateAdd} from 'date-fns';
import {sub as dateSubtract} from 'date-fns/sub';
+// eslint-disable-next-line lodash/import-scope
+import type {Dictionary} from 'lodash';
+import invertBy from 'lodash/invertBy';
import Config from 'react-native-config';
import * as KeyCommand from 'react-native-key-command';
import type {ValueOf} from 'type-fest';
@@ -2171,6 +2174,31 @@ const CONST = {
'_vietNam',
] as string[],
+ NSQS_EXPORT_DATE: {
+ LAST_EXPENSE: 'LAST_EXPENSE',
+ EXPORTED: 'EXPORTED',
+ SUBMITTED: 'SUBMITTED',
+ },
+
+ NSQS_INTEGRATION_ENTITY_MAP_TYPES: {
+ NETSUITE_DEFAULT: 'NETSUITE_DEFAULT',
+ REPORT_FIELD: 'REPORT_FIELD',
+ TAG: 'TAG',
+ },
+
+ NSQS_CONFIG: {
+ AUTO_SYNC: 'autoSync',
+ SYNC_OPTIONS: {
+ MAPPING: {
+ CUSTOMERS: 'syncOptions.mapping.customers',
+ PROJECTS: 'syncOptions.mapping.projects',
+ },
+ },
+ EXPORTER: 'exporter',
+ EXPORT_DATE: 'exportDate',
+ APPROVAL_ACCOUNT: 'approvalAccount',
+ },
+
QUICKBOOKS_EXPORT_DATE: {
LAST_EXPENSE: 'LAST_EXPENSE',
REPORT_EXPORTED: 'REPORT_EXPORTED',
@@ -2657,17 +2685,20 @@ const CONST = {
QBD: 'quickbooksDesktop',
XERO: 'xero',
NETSUITE: 'netsuite',
+ NSQS: 'netsuiteQuickStart',
SAGE_INTACCT: 'intacct',
},
ROUTE: {
QBO: 'quickbooks-online',
XERO: 'xero',
NETSUITE: 'netsuite',
+ NSQS: 'nsqs',
SAGE_INTACCT: 'sage-intacct',
QBD: 'quickbooks-desktop',
},
NAME_USER_FRIENDLY: {
netsuite: 'NetSuite',
+ netsuiteQuickStart: 'NSQS',
quickbooksOnline: 'QuickBooks Online',
quickbooksDesktop: 'QuickBooks Desktop',
xero: 'Xero',
@@ -2745,6 +2776,12 @@ const CONST = {
NETSUITE_SYNC_EXPENSIFY_REIMBURSED_REPORTS: 'netSuiteSyncExpensifyReimbursedReports',
NETSUITE_SYNC_IMPORT_VENDORS_TITLE: 'netSuiteImportVendorsTitle',
NETSUITE_SYNC_IMPORT_CUSTOM_LISTS_TITLE: 'netSuiteImportCustomListsTitle',
+ NSQS_SYNC_CONNECTION: 'nsqsSyncConnection',
+ NSQS_SYNC_ACCOUNTS: 'nsqsSyncAccounts',
+ NSQS_SYNC_EMPLOYEES: 'nsqsSyncEmployees',
+ NSQS_SYNC_CUSTOMERS: 'nsqsSyncCustomers',
+ NSQS_SYNC_PROJECTS: 'nsqsSyncProjects',
+ NSQS_SYNC_CURRENCY: 'nsqsSyncCurrency',
SAGE_INTACCT_SYNC_CHECK_CONNECTION: 'intacctCheckConnection',
SAGE_INTACCT_SYNC_IMPORT_TITLE: 'intacctImportTitle',
SAGE_INTACCT_SYNC_IMPORT_DATA: 'intacctImportData',
@@ -2753,6 +2790,19 @@ const CONST = {
SAGE_INTACCT_SYNC_IMPORT_SYNC_REIMBURSED_REPORTS: 'intacctImportSyncBillPayments',
},
SYNC_STAGE_TIMEOUT_MINUTES: 20,
+
+ // Map each connection to its designated display connection
+ get MULTI_CONNECTIONS_MAPPING() {
+ return {
+ [this.NAME.NETSUITE]: this.NAME.NETSUITE,
+ [this.NAME.NSQS]: this.NAME.NETSUITE,
+ } as Record, ValueOf | undefined>;
+ },
+
+ // Get linked connections by the designated display connection
+ get MULTI_CONNECTIONS_MAPPING_INVERTED() {
+ return invertBy(this.MULTI_CONNECTIONS_MAPPING) as Dictionary> | undefined>;
+ },
},
ACCESS_VARIANTS: {
PAID: 'paid',
@@ -5043,6 +5093,7 @@ const CONST = {
quickbooksOnline: 'QuickBooks Online',
xero: 'Xero',
netsuite: 'NetSuite',
+ netsuiteQuickStart: 'NSQS',
intacct: 'Sage Intacct',
quickbooksDesktop: 'QuickBooks Desktop',
},
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 54b7da704cd1e..1fb84c3dd9cf4 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -727,6 +727,8 @@ const ONYXKEYS = {
NETSUITE_TOKEN_INPUT_FORM_DRAFT: 'netsuiteTokenInputFormDraft',
NETSUITE_CUSTOM_FORM_ID_FORM: 'netsuiteCustomFormIDForm',
NETSUITE_CUSTOM_FORM_ID_FORM_DRAFT: 'netsuiteCustomFormIDFormDraft',
+ NSQS_OAUTH2_FORM: 'nsqsOAuth2Form',
+ NSQS_OAUTH2_FORM_DRAFT: 'nsqsOAuth2FormDraft',
SAGE_INTACCT_DIMENSION_TYPE_FORM: 'sageIntacctDimensionTypeForm',
SAGE_INTACCT_DIMENSION_TYPE_FORM_DRAFT: 'sageIntacctDimensionTypeFormDraft',
SEARCH_ADVANCED_FILTERS_FORM: 'searchAdvancedFiltersForm',
@@ -837,6 +839,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.NETSUITE_CUSTOM_SEGMENT_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm;
[ONYXKEYS.FORMS.NETSUITE_TOKEN_INPUT_FORM]: FormTypes.NetSuiteTokenInputForm;
[ONYXKEYS.FORMS.NETSUITE_CUSTOM_FORM_ID_FORM]: FormTypes.NetSuiteCustomFormIDForm;
+ [ONYXKEYS.FORMS.NSQS_OAUTH2_FORM]: FormTypes.NSQSOAuth2Form;
[ONYXKEYS.FORMS.SAGE_INTACCT_DIMENSION_TYPE_FORM]: FormTypes.SageIntacctDimensionForm;
[ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM]: FormTypes.SearchAdvancedFiltersForm;
[ONYXKEYS.FORMS.TEXT_PICKER_MODAL_FORM]: FormTypes.TextPickerModalForm;
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 393085ab43848..87664b718974d 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1114,6 +1114,28 @@ const ROUTES = {
getRoute: (policyID: string, connection?: ValueOf) =>
`settings/workspaces/${policyID}/accounting/${connection as string}/card-reconciliation/account` as const,
},
+ WORKSPACE_ACCOUNTING_MULTI_CONNECTION_SELECTOR: {
+ route: 'settings/workspaces/:policyID/accounting/:connection/connection-selector',
+ getRoute: (
+ policyID: string,
+ connection: ValueOf,
+ integrationToDisconnect?: ConnectionName,
+ shouldDisconnectIntegrationBeforeConnecting?: boolean,
+ ) => {
+ const searchParams = new URLSearchParams();
+
+ if (integrationToDisconnect) {
+ searchParams.append('integrationToDisconnect', integrationToDisconnect);
+ }
+ if (shouldDisconnectIntegrationBeforeConnecting !== undefined) {
+ searchParams.append('shouldDisconnectIntegrationBeforeConnecting', shouldDisconnectIntegrationBeforeConnecting.toString());
+ }
+
+ const queryParams = searchParams.size ? `?${searchParams.toString()}` : '';
+
+ return `settings/workspaces/${policyID}/accounting/${connection}/connection-selector${queryParams}` as const;
+ },
+ },
WORKSPACE_CATEGORIES: {
route: 'settings/workspaces/:policyID/categories',
getRoute: (policyID: string | undefined) => {
@@ -1942,6 +1964,50 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/connections/netsuite/advanced/autosync/accounting-method',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/connections/netsuite/advanced/autosync/accounting-method` as const,
},
+ POLICY_ACCOUNTING_NSQS_SETUP: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/setup',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/setup` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_IMPORT: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/import',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_IMPORT_CUSTOMERS: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/import/customers',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import/customers` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/import/customers/displayed-as',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import/customers/displayed-as` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_IMPORT_PROJECTS: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/import/projects',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import/projects` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_IMPORT_PROJECTS_DISPLAYED_AS: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/import/projects/displayed-as',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/import/projects/displayed-as` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_EXPORT: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/export',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/export` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_EXPORT_PREFERRED_EXPORTER: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/export/preferred-exporter',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/export/preferred-exporter` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_EXPORT_DATE: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/export/date',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/export/date` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_ADVANCED: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/advanced',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/advanced` as const,
+ },
+ POLICY_ACCOUNTING_NSQS_ADVANCED_APPROVAL_ACCOUNT: {
+ route: 'settings/workspaces/:policyID/accounting/nsqs/advanced/approval-account',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/nsqs/advanced/approval-account` as const,
+ },
POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES: {
route: 'settings/workspaces/:policyID/accounting/sage-intacct/prerequisites',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/sage-intacct/prerequisites` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 04bb3c6297ba9..4ee20f34cf166 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -431,6 +431,17 @@ const SCREENS = {
NETSUITE_CUSTOM_FORM_ID: 'Policy_Accounting_NetSuite_Custom_Form_ID',
NETSUITE_AUTO_SYNC: 'Policy_Accounting_NetSuite_Auto_Sync',
NETSUITE_ACCOUNTING_METHOD: 'Policy_Accounting_NetSuite_Accounting_Method',
+ NSQS_SETUP: 'Policy_Accounting_NSQS_Setup',
+ NSQS_IMPORT: 'Policy_Accounting_NSQS_Import',
+ NSQS_IMPORT_CUSTOMERS: 'Policy_Accounting_NSQS_Import_Customers',
+ NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS: 'Policy_Accounting_NSQS_Import_Customers_Displayed_As',
+ NSQS_IMPORT_PROJECTS: 'Policy_Accounting_NSQS_Import_Projects',
+ NSQS_IMPORT_PROJECTS_DISPLAYED_AS: 'Policy_Accounting_NSQS_Import_Projects_Displayed_As',
+ NSQS_EXPORT: 'Policy_Accounting_NSQS_Export',
+ NSQS_EXPORT_PREFERRED_EXPORTER: 'Policy_Accounting_NSQS_Export_Preferred_Exporter',
+ NSQS_EXPORT_DATE: 'Policy_Accounting_NSQS_Export_Date',
+ NSQS_ADVANCED: 'Policy_Accounting_NSQS_Advanced',
+ NSQS_ADVANCED_APPROVAL_ACCOUNT: 'Policy_Accounting_NSQS_Advanced_Approval_Account',
SAGE_INTACCT_PREREQUISITES: 'Policy_Accounting_Sage_Intacct_Prerequisites',
ENTER_SAGE_INTACCT_CREDENTIALS: 'Policy_Enter_Sage_Intacct_Credentials',
EXISTING_SAGE_INTACCT_CONNECTIONS: 'Policy_Existing_Sage_Intacct_Connections',
@@ -454,6 +465,7 @@ const SCREENS = {
SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account',
CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation',
RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings',
+ MULTI_CONNECTION_SELECTOR: 'Policy_Accounting_Multi_Connection_Selector',
},
INITIAL: 'Workspace_Initial',
PROFILE: 'Workspace_Profile',
diff --git a/src/components/ConnectToNSQSFlow/index.tsx b/src/components/ConnectToNSQSFlow/index.tsx
new file mode 100644
index 0000000000000..87b32007f9f5e
--- /dev/null
+++ b/src/components/ConnectToNSQSFlow/index.tsx
@@ -0,0 +1,15 @@
+import {useEffect} from 'react';
+import Navigation from '@libs/Navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+import type {ConnectToNSQSFlowProps} from './types';
+
+function ConnectToNSQSFlow({policyID}: ConnectToNSQSFlowProps) {
+ useEffect(() => {
+ Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NSQS_SETUP.getRoute(policyID));
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, []);
+
+ return null;
+}
+
+export default ConnectToNSQSFlow;
diff --git a/src/components/ConnectToNSQSFlow/types.ts b/src/components/ConnectToNSQSFlow/types.ts
new file mode 100644
index 0000000000000..7a19bd321b994
--- /dev/null
+++ b/src/components/ConnectToNSQSFlow/types.ts
@@ -0,0 +1,10 @@
+import type {PolicyConnectionName} from '@src/types/onyx/Policy';
+
+type ConnectToNSQSFlowProps = {
+ policyID: string;
+ shouldDisconnectIntegrationBeforeConnecting?: boolean;
+ integrationToDisconnect?: PolicyConnectionName;
+};
+
+// eslint-disable-next-line import/prefer-default-export
+export type {ConnectToNSQSFlowProps};
diff --git a/src/components/ConnectToNetSuiteFlow/index.tsx b/src/components/ConnectToNetSuiteFlow/index.tsx
index 7957896d4006e..1bf3712c0f018 100644
--- a/src/components/ConnectToNetSuiteFlow/index.tsx
+++ b/src/components/ConnectToNetSuiteFlow/index.tsx
@@ -18,7 +18,11 @@ function ConnectToNetSuiteFlow({policyID}: ConnectToNetSuiteFlowProps) {
const {translate} = useLocalize();
const hasPoliciesConnectedToNetSuite = !!getAdminPoliciesConnectedToNetSuite()?.length;
- const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use the correct modal type
+ // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
+ const {isSmallScreenWidth} = useResponsiveLayout();
+
const [isReuseConnectionsPopoverOpen, setIsReuseConnectionsPopoverOpen] = useState(false);
const [reuseConnectionPopoverPosition, setReuseConnectionPopoverPosition] = useState({horizontal: 0, vertical: 0});
const {popoverAnchorRefs} = useAccountingContext();
@@ -57,7 +61,7 @@ function ConnectToNetSuiteFlow({policyID}: ConnectToNetSuiteFlowProps) {
}, []);
if (threeDotsMenuContainerRef) {
- if (!shouldUseNarrowLayout) {
+ if (!isSmallScreenWidth) {
threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => {
const horizontal = x + width;
const vertical = y + height;
diff --git a/src/components/ConnectionLayout.tsx b/src/components/ConnectionLayout.tsx
index 9a232e83fb976..c7bc37e38e3e2 100644
--- a/src/components/ConnectionLayout.tsx
+++ b/src/components/ConnectionLayout.tsx
@@ -5,7 +5,7 @@ import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
+import {getPolicy} from '@libs/PolicyUtils';
import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {TranslationPaths} from '@src/languages/types';
@@ -106,7 +106,7 @@ function ConnectionLayout({
}: ConnectionLayoutProps) {
const {translate} = useLocalize();
- const policy = PolicyUtils.getPolicy(policyID);
+ const policy = getPolicy(policyID);
const isConnectionEmpty = isEmpty(policy?.connections?.[connectionName]);
const renderSelectionContent = useMemo(
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 5cfa87d472da0..da402f612a2c2 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -113,6 +113,7 @@ import ImageCropSquareMask from '@assets/images/image-crop-square-mask.svg';
import Inbox from '@assets/images/inbox.svg';
import Info from '@assets/images/info.svg';
import NetSuiteSquare from '@assets/images/integrationicons/netsuite-icon-square.svg';
+import NSQSSquare from '@assets/images/integrationicons/netsuite-quickstart-icon-square.svg';
import QBDSquare from '@assets/images/integrationicons/qbd-icon-square.svg';
import QBOCircle from '@assets/images/integrationicons/qbo-icon-circle.svg';
import QBOSquare from '@assets/images/integrationicons/qbo-icon-square.svg';
@@ -406,6 +407,7 @@ export {
CheckCircle,
CheckmarkCircle,
NetSuiteSquare,
+ NSQSSquare,
XeroCircle,
QBOCircle,
Filters,
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 40ec431ca8935..32f9f7d5a8272 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -1,5 +1,5 @@
import type {ImageContentFit} from 'expo-image';
-import type {ReactElement, ReactNode} from 'react';
+import type {ReactElement, ReactNode, Ref} from 'react';
import React, {forwardRef, useContext, useMemo} from 'react';
import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
@@ -60,6 +60,10 @@ type NoIcon = {
};
type MenuItemBaseProps = {
+ /* View ref */
+ /* eslint-disable-next-line react/no-unused-prop-types */
+ ref?: Ref;
+
/** Function to fire when component is pressed */
onPress?: (event: GestureResponderEvent | KeyboardEvent) => void | Promise;
diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx
index b2d79b6243acf..21fd73e7353dd 100644
--- a/src/components/MenuItemList.tsx
+++ b/src/components/MenuItemList.tsx
@@ -1,6 +1,7 @@
import React, {useRef} from 'react';
import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native';
import useSingleExecution from '@hooks/useSingleExecution';
+import mergeRefs from '@libs/mergeRefs';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import CONST from '@src/CONST';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
@@ -70,32 +71,32 @@ function MenuItemList({menuItems = [], shouldUseSingleExecution = false, wrapper
};
return (
- <>
- {menuItems.map(({key, ...menuItemProps}) => (
- (
+
+
- ))}
- >
+ wrapperStyle={wrapperStyle}
+ onSecondaryInteraction={menuItemProps.link !== undefined ? (e) => secondaryInteraction(menuItemProps.link, e) : undefined}
+ ref={mergeRefs(ref, popoverAnchor)}
+ shouldBlockSelection={!!menuItemProps.link}
+ icon={icon}
+ iconWidth={iconWidth}
+ iconHeight={iconHeight}
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ {...menuItemProps}
+ disabled={!!menuItemProps.disabled || isExecuting}
+ onPress={shouldUseSingleExecution ? singleExecution(menuItemProps.onPress) : menuItemProps.onPress}
+ />
+
+ ))
);
}
diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx
index 020796085ba41..59532361e42df 100644
--- a/src/components/SelectionScreen.tsx
+++ b/src/components/SelectionScreen.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as PolicyUtils from '@libs/PolicyUtils';
+import {getPolicy} from '@libs/PolicyUtils';
import type {AccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {TranslationPaths} from '@src/languages/types';
@@ -49,6 +49,9 @@ type SelectionScreenProps = {
/** Default renderer for every item in the list */
listItem: typeof RadioListItem | typeof UserListItem | typeof TableListItem;
+ /** The style is applied for the wrap component of list item */
+ listItemWrapperStyle?: StyleProp;
+
/** Item `keyForList` to focus initially */
initiallyFocusedOptionKey?: string | null | undefined;
@@ -56,10 +59,10 @@ type SelectionScreenProps = {
onSelectRow: (selection: SelectorType) => void;
/** Callback to fire when back button is pressed */
- onBackButtonPress: () => void;
+ onBackButtonPress?: () => void;
/** The current policyID */
- policyID: string;
+ policyID?: string;
/** Defines which types of access should be verified */
accessVariants?: AccessVariant[];
@@ -115,6 +118,7 @@ function SelectionScreen({
listFooterContent,
sections,
listItem,
+ listItemWrapperStyle,
initiallyFocusedOptionKey,
onSelectRow,
onBackButtonPress,
@@ -138,7 +142,7 @@ function SelectionScreen({
const {translate} = useLocalize();
const styles = useThemeStyles();
- const policy = PolicyUtils.getPolicy(policyID);
+ const policy = getPolicy(policyID);
const isConnectionEmpty = isEmpty(policy?.connections?.[connectionName]);
return (
@@ -180,6 +184,7 @@ function SelectionScreen({
shouldSingleExecuteRowSelect={shouldSingleExecuteRowSelect}
shouldUpdateFocusedIndex={shouldUpdateFocusedIndex}
isAlternateTextMultilineSupported
+ listItemWrapperStyle={listItemWrapperStyle}
>
`${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} setup`,
+ description: ({connectionName}: ConnectionNameParams) => `Select your ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} version to continue.`,
+ },
type: {
free: 'Free',
control: 'Control',
@@ -3974,6 +4042,7 @@ const translations = {
qbd: 'QuickBooks Desktop',
xero: 'Xero',
netsuite: 'NetSuite',
+ nsqs: 'NSQS',
intacct: 'Sage Intacct',
talkYourOnboardingSpecialist: 'Chat with your setup specialist.',
talkYourAccountManager: 'Chat with your account manager.',
@@ -3987,6 +4056,8 @@ const translations = {
return 'Xero';
case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
return 'NetSuite';
+ case CONST.POLICY.CONNECTIONS.NAME.NSQS:
+ return 'NSQS';
case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT:
return 'Sage Intacct';
default: {
@@ -4021,6 +4092,8 @@ const translations = {
return "Can't connect to Xero.";
case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
return "Can't connect to NetSuite.";
+ case CONST.POLICY.CONNECTIONS.NAME.NSQS:
+ return "Can't connect to NSQS.";
case CONST.POLICY.CONNECTIONS.NAME.QBD:
return "Can't connect to QuickBooks Desktop.";
default: {
@@ -4148,6 +4221,7 @@ const translations = {
case 'netSuiteSyncData':
return 'Importing data into Expensify';
case 'netSuiteSyncAccounts':
+ case 'nsqsSyncAccounts':
return 'Syncing accounts';
case 'netSuiteSyncCurrencies':
return 'Syncing currencies';
@@ -4174,6 +4248,16 @@ const translations = {
case 'netSuiteSyncImportVendors':
case 'quickbooksDesktopImportVendors':
return 'Importing vendors';
+ case 'nsqsSyncConnection':
+ return 'Initializing connection to NSQS';
+ case 'nsqsSyncEmployees':
+ return 'Syncing employees';
+ case 'nsqsSyncCustomers':
+ return 'Syncing customers';
+ case 'nsqsSyncProjects':
+ return 'Syncing projects';
+ case 'nsqsSyncCurrency':
+ return 'Syncing currency';
case 'intacctCheckConnection':
return 'Checking Sage Intacct connection';
case 'intacctImportDimensions':
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 581857ba2ac96..76ae8aeac316e 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -3399,6 +3399,70 @@ const translations = {
},
},
},
+ nsqs: {
+ setup: {
+ title: 'NSQS configuración',
+ description: 'Introduce tu ID de cuenta de NSQS',
+ formInputs: {
+ netSuiteAccountID: 'ID de Cuenta NSQS',
+ },
+ },
+ import: {
+ expenseCategories: 'Categorías de gastos',
+ expenseCategoriesDescription: 'Las categorías de gastos de NSQS se importan a Expensify como categorías.',
+ importTypes: {
+ [CONST.NSQS_INTEGRATION_ENTITY_MAP_TYPES.TAG]: {
+ label: 'Etiquetas',
+ description: 'Nivel de línea de pedido',
+ },
+ [CONST.NSQS_INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: {
+ label: 'Campos de informe',
+ description: 'Nivel de informe',
+ },
+ },
+ importFields: {
+ customers: {
+ title: 'Clientes',
+ subtitle: 'Elige cómo gestionar los *clientes* de NSQS en Expensify.',
+ },
+ projects: {
+ title: 'Proyectos',
+ subtitle: 'Elige cómo gestionar los *proyectos* de NSQS en Expensify.',
+ },
+ },
+ },
+ export: {
+ description: 'Configura cómo se exportan los datos de Expensify a NSQS.',
+ exportDate: {
+ label: 'Fecha de exportación',
+ description: 'Usa esta fecha al exportar informe a NSQS.',
+ values: {
+ [CONST.NSQS_EXPORT_DATE.LAST_EXPENSE]: {
+ label: 'Fecha del último gasto',
+ description: 'Fecha del gasto mas reciente en el informe.',
+ },
+ [CONST.NSQS_EXPORT_DATE.EXPORTED]: {
+ label: 'Fecha de exportación',
+ description: 'Fecha de exportación del informe a NSQS.',
+ },
+ [CONST.NSQS_EXPORT_DATE.SUBMITTED]: {
+ label: 'Fecha de envío',
+ description: 'Fecha en la que el informe se envió para su aprobación.',
+ },
+ },
+ },
+ expense: 'Gasto',
+ reimbursableExpenses: 'Exportar gastos reembolsables como',
+ nonReimbursableExpenses: 'Exportar gastos no reembolsables como',
+ },
+ advanced: {
+ autoSyncDescription: 'Sincroniza NSQS y Expensify automáticamente, todos los días. Exporta el informe finalizado en tiempo real',
+ defaultApprovalAccount: 'Preferencia predeterminada de NSQS',
+ approvalAccount: 'Cuenta de aprobación de cuentas por pagar',
+ approvalAccountDescription:
+ 'Elija la cuenta con la que se aprobarán las transacciones en NSQS. Si está sincronizando informes reembolsados, esta es también la cuenta con la que se crearán los pagos de facturas.',
+ },
+ },
intacct: {
sageIntacctSetup: 'Sage Intacct configuración',
prerequisitesTitle: 'Antes de conectar...',
@@ -3446,6 +3510,10 @@ const translations = {
}
},
},
+ multiConnectionSelector: {
+ title: ({connectionName}: ConnectionNameParams) => `${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} configuración`,
+ description: ({connectionName}: ConnectionNameParams) => `Selecciona tu versión de ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} para continuar.`,
+ },
type: {
free: 'Gratis',
control: 'Controlar',
@@ -3981,6 +4049,7 @@ const translations = {
qbd: 'QuickBooks Desktop',
xero: 'Xero',
netsuite: 'NetSuite',
+ nsqs: 'NSQS',
intacct: 'Sage Intacct',
talkYourOnboardingSpecialist: 'Chatea con tu especialista asignado.',
talkYourAccountManager: 'Chatea con tu gestor de cuenta.',
@@ -3994,6 +4063,8 @@ const translations = {
return 'Xero';
case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
return 'NetSuite';
+ case CONST.POLICY.CONNECTIONS.NAME.NSQS:
+ return 'NSQS';
case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT:
return 'Sage Intacct';
default: {
@@ -4027,6 +4098,8 @@ const translations = {
return 'No se puede conectar a Xero.';
case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
return 'No se puede conectar a NetSuite.';
+ case CONST.POLICY.CONNECTIONS.NAME.NSQS:
+ return 'No se puede conectar a NSQS.';
case CONST.POLICY.CONNECTIONS.NAME.QBD:
return 'No se puede conectar a QuickBooks Desktop.';
default: {
@@ -4154,6 +4227,7 @@ const translations = {
case 'netSuiteSyncData':
return 'Importando datos a Expensify';
case 'netSuiteSyncAccounts':
+ case 'nsqsSyncAccounts':
return 'Sincronizando cuentas';
case 'netSuiteSyncCurrencies':
return 'Sincronizando divisas';
@@ -4174,6 +4248,16 @@ const translations = {
case 'netSuiteSyncImportVendors':
case 'quickbooksDesktopImportVendors':
return 'Importando proveedores';
+ case 'nsqsSyncConnection':
+ return 'Iniciando conexión a NSQS';
+ case 'nsqsSyncEmployees':
+ return 'Sincronizando empleados';
+ case 'nsqsSyncCustomers':
+ return 'Sincronizando clientes';
+ case 'nsqsSyncProjects':
+ return 'Sincronizando proyectos';
+ case 'nsqsSyncCurrency':
+ return 'Sincronizando moneda';
case 'netSuiteSyncExpensifyReimbursedReports':
return 'Marcando facturas y recibos de NetSuite como pagados';
case 'netSuiteImportVendorsTitle':
diff --git a/src/libs/API/parameters/ConnectPolicyToNSQSParams.ts b/src/libs/API/parameters/ConnectPolicyToNSQSParams.ts
new file mode 100644
index 0000000000000..16a7b93b116d6
--- /dev/null
+++ b/src/libs/API/parameters/ConnectPolicyToNSQSParams.ts
@@ -0,0 +1,6 @@
+type ConnectPolicyToNSQSParams = {
+ policyID: string;
+ netSuiteAccountID: string;
+};
+
+export default ConnectPolicyToNSQSParams;
diff --git a/src/libs/API/parameters/SyncPolicyToNSQSParams.ts b/src/libs/API/parameters/SyncPolicyToNSQSParams.ts
new file mode 100644
index 0000000000000..319ccb2f1d502
--- /dev/null
+++ b/src/libs/API/parameters/SyncPolicyToNSQSParams.ts
@@ -0,0 +1,6 @@
+type SyncPolicyToNSQSParams = {
+ policyID: string;
+ idempotencyKey: string;
+};
+
+export default SyncPolicyToNSQSParams;
diff --git a/src/libs/API/parameters/UpdateNSQSApprovalAccountParams.ts b/src/libs/API/parameters/UpdateNSQSApprovalAccountParams.ts
new file mode 100644
index 0000000000000..3712e782e143e
--- /dev/null
+++ b/src/libs/API/parameters/UpdateNSQSApprovalAccountParams.ts
@@ -0,0 +1,6 @@
+type UpdateNSQSApprovalAccountParams = {
+ policyID: string;
+ value: string;
+};
+
+export default UpdateNSQSApprovalAccountParams;
diff --git a/src/libs/API/parameters/UpdateNSQSAutoSyncParams.ts b/src/libs/API/parameters/UpdateNSQSAutoSyncParams.ts
new file mode 100644
index 0000000000000..eda70db5027b8
--- /dev/null
+++ b/src/libs/API/parameters/UpdateNSQSAutoSyncParams.ts
@@ -0,0 +1,6 @@
+type UpdateNSQSAutoSyncParams = {
+ policyID: string;
+ enabled: boolean;
+};
+
+export default UpdateNSQSAutoSyncParams;
diff --git a/src/libs/API/parameters/UpdateNSQSCustomersMappingParams.ts b/src/libs/API/parameters/UpdateNSQSCustomersMappingParams.ts
new file mode 100644
index 0000000000000..2d4dfaa288620
--- /dev/null
+++ b/src/libs/API/parameters/UpdateNSQSCustomersMappingParams.ts
@@ -0,0 +1,9 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type UpdateNSQSCustomersMappingParams = {
+ policyID: string;
+ mapping: ValueOf;
+};
+
+export default UpdateNSQSCustomersMappingParams;
diff --git a/src/libs/API/parameters/UpdateNSQSExportDateParams.ts b/src/libs/API/parameters/UpdateNSQSExportDateParams.ts
new file mode 100644
index 0000000000000..56aaca69472b2
--- /dev/null
+++ b/src/libs/API/parameters/UpdateNSQSExportDateParams.ts
@@ -0,0 +1,9 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type UpdateNSQSExportDateParams = {
+ policyID: string;
+ value: ValueOf;
+};
+
+export default UpdateNSQSExportDateParams;
diff --git a/src/libs/API/parameters/UpdateNSQSExporterParams.ts b/src/libs/API/parameters/UpdateNSQSExporterParams.ts
new file mode 100644
index 0000000000000..73ef027d180b6
--- /dev/null
+++ b/src/libs/API/parameters/UpdateNSQSExporterParams.ts
@@ -0,0 +1,6 @@
+type UpdateNSQSExporterParams = {
+ policyID: string;
+ email: string;
+};
+
+export default UpdateNSQSExporterParams;
diff --git a/src/libs/API/parameters/UpdateNSQSProjectsMappingParams.ts b/src/libs/API/parameters/UpdateNSQSProjectsMappingParams.ts
new file mode 100644
index 0000000000000..ee1be53150abe
--- /dev/null
+++ b/src/libs/API/parameters/UpdateNSQSProjectsMappingParams.ts
@@ -0,0 +1,9 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type UpdateNSQSProjectsMappingParams = {
+ policyID: string;
+ mapping: ValueOf;
+};
+
+export default UpdateNSQSProjectsMappingParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index f6c547a45511e..5805c90c4b32c 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -276,6 +276,14 @@ export type {default as RequestExpensifyCardLimitIncreaseParams} from './Request
export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams';
export type {default as CancelBillingSubscriptionParams} from './CancelBillingSubscriptionParams';
export type {default as UpdateNetSuiteCustomFormIDParams} from './UpdateNetSuiteCustomFormIDParams';
+export type {default as ConnectPolicyToNSQSParams} from './ConnectPolicyToNSQSParams';
+export type {default as SyncPolicyToNSQSParams} from './SyncPolicyToNSQSParams';
+export type {default as UpdateNSQSCustomersMappingParams} from './UpdateNSQSCustomersMappingParams';
+export type {default as UpdateNSQSProjectsMappingParams} from './UpdateNSQSProjectsMappingParams';
+export type {default as UpdateNSQSExporterParams} from './UpdateNSQSExporterParams';
+export type {default as UpdateNSQSExportDateParams} from './UpdateNSQSExportDateParams';
+export type {default as UpdateNSQSAutoSyncParams} from './UpdateNSQSAutoSyncParams';
+export type {default as UpdateNSQSApprovalAccountParams} from './UpdateNSQSApprovalAccountParams';
export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIntacctGenericTypeParams';
export type {default as UpdateNetSuiteCustomersJobsParams} from './UpdateNetSuiteCustomersJobsParams';
export type {default as CopyExistingPolicyConnectionParams} from './CopyExistingPolicyConnectionParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 1e2a087955628..616d1f1caac12 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -370,6 +370,13 @@ const WRITE_COMMANDS = {
UPDATE_NETSUITE_APPROVAL_ACCOUNT: 'UpdateNetSuiteApprovalAccount',
UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE: 'UpdateNetSuiteCustomFormIDOptionsReimbursable',
UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE: 'UpdateNetSuiteCustomFormIDOptionsNonReimbursable',
+ CONNECT_POLICY_TO_NSQS: 'ConnectPolicyToNSQS',
+ UPDATE_NSQS_CUSTOMERS_MAPPING: 'UpdateNSQSCustomersMapping',
+ UPDATE_NSQS_PROJECTS_MAPPING: 'UpdateNSQSProjectsMapping',
+ UPDATE_NSQS_EXPORTER: 'UpdateNSQSExporter',
+ UPDATE_NSQS_EXPORT_DATE: 'UpdateNSQSExportDate',
+ UPDATE_NSQS_AUTO_SYNC: 'UpdateNSQSAutoSync',
+ UPDATE_NSQS_APPROVAL_ACCOUNT: 'UpdateNSQSApprovalAccount',
REQUEST_EXPENSIFY_CARD_LIMIT_INCREASE: 'RequestExpensifyCardLimitIncrease',
CONNECT_POLICY_TO_SAGE_INTACCT: 'ConnectPolicyToSageIntacct',
COPY_EXISTING_POLICY_CONNECTION: 'CopyExistingPolicyConnection',
@@ -859,6 +866,13 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_NETSUITE_APPROVAL_ACCOUNT]: Parameters.UpdateNetSuiteGenericTypeParams<'value', string>;
[WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_REIMBURSABLE]: Parameters.UpdateNetSuiteCustomFormIDParams;
[WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOM_FORM_ID_OPTIONS_NON_REIMBURSABLE]: Parameters.UpdateNetSuiteCustomFormIDParams;
+ [WRITE_COMMANDS.CONNECT_POLICY_TO_NSQS]: Parameters.ConnectPolicyToNSQSParams;
+ [WRITE_COMMANDS.UPDATE_NSQS_CUSTOMERS_MAPPING]: Parameters.UpdateNSQSCustomersMappingParams;
+ [WRITE_COMMANDS.UPDATE_NSQS_PROJECTS_MAPPING]: Parameters.UpdateNSQSProjectsMappingParams;
+ [WRITE_COMMANDS.UPDATE_NSQS_EXPORTER]: Parameters.UpdateNSQSExporterParams;
+ [WRITE_COMMANDS.UPDATE_NSQS_EXPORT_DATE]: Parameters.UpdateNSQSExportDateParams;
+ [WRITE_COMMANDS.UPDATE_NSQS_AUTO_SYNC]: Parameters.UpdateNSQSAutoSyncParams;
+ [WRITE_COMMANDS.UPDATE_NSQS_APPROVAL_ACCOUNT]: Parameters.UpdateNSQSApprovalAccountParams;
[WRITE_COMMANDS.UPDATE_SAGE_INTACCT_ENTITY]: Parameters.UpdateSageIntacctGenericTypeParams<'entity', string>;
[WRITE_COMMANDS.UPDATE_SAGE_INTACCT_BILLABLE]: Parameters.UpdateSageIntacctGenericTypeParams<'enabled', boolean>;
[WRITE_COMMANDS.UPDATE_SAGE_INTACCT_DEPARTMENT_MAPPING]: Parameters.UpdateSageIntacctGenericTypeParams<'mapping', SageIntacctMappingValue>;
@@ -922,6 +936,7 @@ const READ_COMMANDS = {
SYNC_POLICY_TO_QUICKBOOKS_ONLINE: 'SyncPolicyToQuickbooksOnline',
SYNC_POLICY_TO_XERO: 'SyncPolicyToXero',
SYNC_POLICY_TO_NETSUITE: 'SyncPolicyToNetSuite',
+ SYNC_POLICY_TO_NSQS: 'SyncPolicyToNSQS',
SYNC_POLICY_TO_SAGE_INTACCT: 'SyncPolicyToSageIntacct',
SYNC_POLICY_TO_QUICKBOOKS_DESKTOP: 'SyncPolicyToQuickbooksDesktop',
OPEN_REIMBURSEMENT_ACCOUNT_PAGE: 'OpenReimbursementAccountPage',
@@ -988,6 +1003,7 @@ type ReadCommandParameters = {
[READ_COMMANDS.SYNC_POLICY_TO_QUICKBOOKS_ONLINE]: Parameters.SyncPolicyToQuickbooksOnlineParams;
[READ_COMMANDS.SYNC_POLICY_TO_XERO]: Parameters.SyncPolicyToXeroParams;
[READ_COMMANDS.SYNC_POLICY_TO_NETSUITE]: Parameters.SyncPolicyToNetSuiteParams;
+ [READ_COMMANDS.SYNC_POLICY_TO_NSQS]: Parameters.SyncPolicyToNSQSParams;
[READ_COMMANDS.SYNC_POLICY_TO_SAGE_INTACCT]: Parameters.SyncPolicyToNetSuiteParams;
[READ_COMMANDS.SYNC_POLICY_TO_QUICKBOOKS_DESKTOP]: Parameters.SyncPolicyToQuickbooksDesktopParams;
[READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE]: Parameters.OpenReimbursementAccountPageParams;
diff --git a/src/libs/AccountingUtils.ts b/src/libs/AccountingUtils.ts
index 7516048241d6a..6f1023a1c1363 100644
--- a/src/libs/AccountingUtils.ts
+++ b/src/libs/AccountingUtils.ts
@@ -7,6 +7,7 @@ const ROUTE_NAME_MAPPING = {
[CONST.POLICY.CONNECTIONS.ROUTE.XERO]: CONST.POLICY.CONNECTIONS.NAME.XERO,
[CONST.POLICY.CONNECTIONS.ROUTE.SAGE_INTACCT]: CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT,
[CONST.POLICY.CONNECTIONS.ROUTE.NETSUITE]: CONST.POLICY.CONNECTIONS.NAME.NETSUITE,
+ [CONST.POLICY.CONNECTIONS.ROUTE.NSQS]: CONST.POLICY.CONNECTIONS.NAME.NSQS,
[CONST.POLICY.CONNECTIONS.ROUTE.QBD]: CONST.POLICY.CONNECTIONS.NAME.QBD,
};
@@ -15,6 +16,7 @@ const NAME_ROUTE_MAPPING = {
[CONST.POLICY.CONNECTIONS.NAME.XERO]: CONST.POLICY.CONNECTIONS.ROUTE.XERO,
[CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]: CONST.POLICY.CONNECTIONS.ROUTE.SAGE_INTACCT,
[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: CONST.POLICY.CONNECTIONS.ROUTE.NETSUITE,
+ [CONST.POLICY.CONNECTIONS.NAME.NSQS]: CONST.POLICY.CONNECTIONS.ROUTE.NSQS,
[CONST.POLICY.CONNECTIONS.NAME.QBD]: CONST.POLICY.CONNECTIONS.ROUTE.QBD,
};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index dae1f68d9082d..e3b5524f8bba1 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -499,6 +499,21 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteAutoSyncPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_ACCOUNTING_METHOD]: () =>
require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteAccountingMethodPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_SETUP]: () => require('../../../../pages/workspace/accounting/nsqs/NSQSSetupPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT]: () => require('../../../../pages/workspace/accounting/nsqs/import/NSQSImportPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_CUSTOMERS]: () => require('../../../../pages/workspace/accounting/nsqs/import/NSQSCustomersPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS]: () =>
+ require('../../../../pages/workspace/accounting/nsqs/import/NSQSCustomersDisplayedAsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_PROJECTS]: () => require('../../../../pages/workspace/accounting/nsqs/import/NSQSProjectsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_PROJECTS_DISPLAYED_AS]: () =>
+ require('../../../../pages/workspace/accounting/nsqs/import/NSQSProjectsDisplayedAsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT]: () => require('../../../../pages/workspace/accounting/nsqs/export/NSQSExportPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT_PREFERRED_EXPORTER]: () =>
+ require('../../../../pages/workspace/accounting/nsqs/export/NSQSPreferredExporterPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT_DATE]: () => require('../../../../pages/workspace/accounting/nsqs/export/NSQSDatePage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_ADVANCED]: () => require('../../../../pages/workspace/accounting/nsqs/advanced/NSQSAdvancedPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_ADVANCED_APPROVAL_ACCOUNT]: () =>
+ require('../../../../pages/workspace/accounting/nsqs/advanced/NSQSApprovalAccountPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: () => require('../../../../pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS]: () =>
require('../../../../pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage').default,
@@ -526,6 +541,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/reconciliation/CardReconciliationPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: () =>
require('../../../../pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.MULTI_CONNECTION_SELECTOR]: () => require('../../../../pages/workspace/accounting/MultiConnectionSelectorPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default,
[SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../../pages/workspace/taxes/WorkspaceEditTaxPage').default,
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 7865993d08e99..79aed5bcf7567 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -124,6 +124,17 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_CUSTOM_FORM_ID,
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_AUTO_SYNC,
SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_ACCOUNTING_METHOD,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_SETUP,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_CUSTOMERS,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_PROJECTS,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_PROJECTS_DISPLAYED_AS,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT_PREFERRED_EXPORTER,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT_DATE,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_ADVANCED,
+ SCREENS.WORKSPACE.ACCOUNTING.NSQS_ADVANCED_APPROVAL_ACCOUNT,
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES,
SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS,
SCREENS.WORKSPACE.ACCOUNTING.EXISTING_SAGE_INTACCT_CONNECTIONS,
@@ -147,6 +158,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT,
SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION,
SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS,
+ SCREENS.WORKSPACE.ACCOUNTING.MULTI_CONNECTION_SELECTOR,
],
[SCREENS.WORKSPACE.TAXES]: [
SCREENS.WORKSPACE.TAXES_SETTINGS,
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 81e0bb4889c89..1838b85fcee0d 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -566,6 +566,39 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_ACCOUNTING_METHOD]: {
path: ROUTES.POLICY_ACCOUNTING_NETSUITE_ACCOUNTING_METHOD.route,
},
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_SETUP]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_SETUP.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_IMPORT.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_CUSTOMERS]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_IMPORT_CUSTOMERS.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_PROJECTS]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_IMPORT_PROJECTS.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_PROJECTS_DISPLAYED_AS]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_IMPORT_PROJECTS_DISPLAYED_AS.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_EXPORT.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT_PREFERRED_EXPORTER]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_EXPORT_PREFERRED_EXPORTER.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT_DATE]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_EXPORT_DATE.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_ADVANCED]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_ADVANCED.route,
+ },
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_ADVANCED_APPROVAL_ACCOUNT]: {
+ path: ROUTES.POLICY_ACCOUNTING_NSQS_ADVANCED_APPROVAL_ACCOUNT.route,
+ },
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES.route},
[SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ENTER_CREDENTIALS.route},
[SCREENS.WORKSPACE.ACCOUNTING.EXISTING_SAGE_INTACCT_CONNECTIONS]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXISTING_CONNECTIONS.route},
@@ -591,6 +624,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.route},
[SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: {path: ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.route},
[SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {path: ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.MULTI_CONNECTION_SELECTOR]: {path: ROUTES.WORKSPACE_ACCOUNTING_MULTI_CONNECTION_SELECTOR.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 2e75b1e36280a..f57c4c3f57158 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -696,6 +696,39 @@ type SettingsNavigatorParamList = {
policyID: string;
expenseType: ValueOf;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_SETUP]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_CUSTOMERS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_CUSTOMERS_DISPLAYED_AS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_PROJECTS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_IMPORT_PROJECTS_DISPLAYED_AS]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT_PREFERRED_EXPORTER]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_EXPORT_DATE]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_ADVANCED]: {
+ policyID: string;
+ };
+ [SCREENS.WORKSPACE.ACCOUNTING.NSQS_ADVANCED_APPROVAL_ACCOUNT]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: {
policyID: string;
};
@@ -760,6 +793,12 @@ type SettingsNavigatorParamList = {
policyID: string;
connection: ValueOf;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.MULTI_CONNECTION_SELECTOR]: {
+ policyID: string;
+ connection: ValueOf;
+ integrationToDisconnect?: ConnectionName;
+ shouldDisconnectIntegrationBeforeConnecting?: boolean;
+ };
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index a84b437a65f7b..b3035681d005a 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -12,7 +12,7 @@ import type {SvgProps} from 'react-native-svg';
import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from 'src/types/onyx/OriginalMessage';
import type {SetRequired, TupleToUnion, ValueOf} from 'type-fest';
import type {FileObject} from '@components/AttachmentModal';
-import {FallbackAvatar, IntacctSquare, NetSuiteSquare, QBOSquare, XeroSquare} from '@components/Icon/Expensicons';
+import {FallbackAvatar, IntacctSquare, NetSuiteSquare, NSQSSquare, QBOSquare, XeroSquare} from '@components/Icon/Expensicons';
import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars';
import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars';
import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput';
@@ -8797,6 +8797,9 @@ function getIntegrationIcon(connectionName?: ConnectionName) {
if (connectionName === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) {
return NetSuiteSquare;
}
+ if (connectionName === CONST.POLICY.CONNECTIONS.NAME.NSQS) {
+ return NSQSSquare;
+ }
if (connectionName === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT) {
return IntacctSquare;
}
diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts
index 35272f6c231c0..f848af54646a2 100644
--- a/src/libs/actions/Policy/Policy.ts
+++ b/src/libs/actions/Policy/Policy.ts
@@ -689,6 +689,10 @@ function clearNetSuiteAutoSyncErrorField(policyID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuite: {config: {errorFields: {autoSync: null}}}}});
}
+function clearNSQSErrorField(policyID: string, fieldName: string) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuiteQuickStart: {config: {errorFields: {[fieldName]: null}}}}});
+}
+
function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserEmail: string) {
const policy = getPolicy(policyID);
@@ -4856,6 +4860,7 @@ export {
updateWorkspaceDescription,
setWorkspacePayer,
setWorkspaceReimbursement,
+ clearNSQSErrorField,
openPolicyWorkflowsPage,
enableCompanyCards,
enablePolicyConnections,
diff --git a/src/libs/actions/connections/NSQS.ts b/src/libs/actions/connections/NSQS.ts
new file mode 100644
index 0000000000000..740be3ddb01fc
--- /dev/null
+++ b/src/libs/actions/connections/NSQS.ts
@@ -0,0 +1,187 @@
+import Onyx from 'react-native-onyx';
+import type {OnyxUpdate} from 'react-native-onyx';
+import type {PartialDeep, ValueOf} from 'type-fest';
+import * as API from '@libs/API';
+import type {ConnectPolicyToNSQSParams} from '@libs/API/parameters';
+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 {Connections} from '@src/types/onyx/Policy';
+
+function connectPolicyToNSQS(policyID: string, nsqsAccountID: string) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`,
+ value: {
+ stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.NSQS_SYNC_CONNECTION,
+ connectionName: CONST.POLICY.CONNECTIONS.NAME.NSQS,
+ timestamp: new Date().toISOString(),
+ },
+ },
+ ];
+
+ const params: ConnectPolicyToNSQSParams = {
+ policyID,
+ netSuiteAccountID: nsqsAccountID,
+ };
+
+ API.write(WRITE_COMMANDS.CONNECT_POLICY_TO_NSQS, params, {optimisticData});
+}
+
+function buildOnyxDataForNSQSConfiguration(
+ policyID: string,
+ settingName: TSettingName,
+ settingValue: PartialDeep,
+ oldSettingValue: PartialDeep,
+ fieldName: string,
+) {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ [CONST.POLICY.CONNECTIONS.NAME.NSQS]: {
+ config: {
+ [settingName]: settingValue ?? null,
+ pendingFields: {
+ [fieldName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ errorFields: {
+ [fieldName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ [CONST.POLICY.CONNECTIONS.NAME.NSQS]: {
+ config: {
+ [settingName]: oldSettingValue ?? null,
+ pendingFields: {
+ [fieldName]: null,
+ },
+ errorFields: {
+ [fieldName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ value: {
+ connections: {
+ [CONST.POLICY.CONNECTIONS.NAME.NSQS]: {
+ config: {
+ pendingFields: {
+ [fieldName]: null,
+ },
+ },
+ },
+ },
+ },
+ },
+ ];
+
+ return {
+ optimisticData,
+ failureData,
+ successData,
+ };
+}
+
+function updateNSQSCustomersMapping(policyID: string, mapping: ValueOf, oldMapping: ValueOf) {
+ const onyxData = buildOnyxDataForNSQSConfiguration(
+ policyID,
+ 'syncOptions',
+ {mapping: {customers: mapping}},
+ {mapping: {customers: oldMapping}},
+ CONST.NSQS_CONFIG.SYNC_OPTIONS.MAPPING.CUSTOMERS,
+ );
+
+ const params = {
+ policyID,
+ mapping,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_NSQS_CUSTOMERS_MAPPING, params, onyxData);
+}
+
+function updateNSQSProjectsMapping(policyID: string, mapping: ValueOf, oldMapping: ValueOf) {
+ const onyxData = buildOnyxDataForNSQSConfiguration(
+ policyID,
+ 'syncOptions',
+ {mapping: {projects: mapping}},
+ {mapping: {projects: oldMapping}},
+ CONST.NSQS_CONFIG.SYNC_OPTIONS.MAPPING.PROJECTS,
+ );
+
+ const params = {
+ policyID,
+ mapping,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_NSQS_PROJECTS_MAPPING, params, onyxData);
+}
+
+function updateNSQSExporter(policyID: string, email: string, oldEmail: string) {
+ const onyxData = buildOnyxDataForNSQSConfiguration(policyID, 'exporter', email, oldEmail, CONST.NSQS_CONFIG.EXPORTER);
+
+ const params = {
+ policyID,
+ email,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_NSQS_EXPORTER, params, onyxData);
+}
+
+function updateNSQSExportDate(policyID: string, value: ValueOf, oldValue: ValueOf) {
+ const onyxData = buildOnyxDataForNSQSConfiguration(policyID, 'exportDate', value, oldValue, CONST.NSQS_CONFIG.EXPORT_DATE);
+
+ const params = {
+ policyID,
+ value,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_NSQS_EXPORT_DATE, params, onyxData);
+}
+
+function updateNSQSAutoSync(policyID: string, enabled: boolean) {
+ const onyxData = buildOnyxDataForNSQSConfiguration(policyID, 'autoSync', {enabled}, {enabled: !enabled}, CONST.NSQS_CONFIG.AUTO_SYNC);
+
+ const params = {
+ policyID,
+ enabled,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_NSQS_AUTO_SYNC, params, onyxData);
+}
+
+function updateNSQSApprovalAccount(policyID: string, value: string, oldValue: string) {
+ const onyxData = buildOnyxDataForNSQSConfiguration(policyID, 'approvalAccount', value, oldValue, CONST.NSQS_CONFIG.APPROVAL_ACCOUNT);
+
+ const params = {
+ policyID,
+ value,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_NSQS_APPROVAL_ACCOUNT, params, onyxData);
+}
+
+export {connectPolicyToNSQS, updateNSQSCustomersMapping, updateNSQSProjectsMapping, updateNSQSExporter, updateNSQSExportDate, updateNSQSAutoSync, updateNSQSApprovalAccount};
diff --git a/src/libs/actions/connections/index.ts b/src/libs/actions/connections/index.ts
index 9087b9fb00c89..5d1131b068361 100644
--- a/src/libs/actions/connections/index.ts
+++ b/src/libs/actions/connections/index.ts
@@ -205,6 +205,9 @@ function getSyncConnectionParameters(connectionName: PolicyConnectionName) {
case CONST.POLICY.CONNECTIONS.NAME.NETSUITE: {
return {readCommand: READ_COMMANDS.SYNC_POLICY_TO_NETSUITE, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.NETSUITE_SYNC_CONNECTION};
}
+ case CONST.POLICY.CONNECTIONS.NAME.NSQS: {
+ return {readCommand: READ_COMMANDS.SYNC_POLICY_TO_NSQS, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.NSQS_SYNC_CONNECTION};
+ }
case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: {
return {readCommand: READ_COMMANDS.SYNC_POLICY_TO_SAGE_INTACCT, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.SAGE_INTACCT_SYNC_CHECK_CONNECTION};
}
diff --git a/src/pages/settings/AboutPage/AboutPage.tsx b/src/pages/settings/AboutPage/AboutPage.tsx
index 4b5d06da4b689..b2af2c0dfb2b7 100644
--- a/src/pages/settings/AboutPage/AboutPage.tsx
+++ b/src/pages/settings/AboutPage/AboutPage.tsx
@@ -1,7 +1,7 @@
import React, {useCallback, useMemo, useRef} from 'react';
import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
-import type {GestureResponderEvent, Text as RNText, StyleProp, ViewStyle} from 'react-native';
+import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -17,11 +17,11 @@ import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useWaitForNavigation from '@hooks/useWaitForNavigation';
-import * as Environment from '@libs/Environment/Environment';
+import {isInternalTestBuild} from '@libs/Environment/Environment';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
-import * as Link from '@userActions/Link';
-import * as Report from '@userActions/Report';
+import {openExternalLink} from '@userActions/Link';
+import {navigateToConciergeChat} from '@userActions/Report';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ROUTES from '@src/ROUTES';
@@ -51,7 +51,7 @@ type MenuItem = {
function AboutPage() {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const popoverAnchor = useRef(null);
+ const popoverAnchor = useRef(null);
const waitForNavigate = useWaitForNavigation();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -72,7 +72,7 @@ function AboutPage() {
icon: Expensicons.Eye,
iconRight: Expensicons.NewWindow,
action: () => {
- Link.openExternalLink(CONST.GITHUB_URL);
+ openExternalLink(CONST.GITHUB_URL);
return Promise.resolve();
},
link: CONST.GITHUB_URL,
@@ -82,7 +82,7 @@ function AboutPage() {
icon: Expensicons.MoneyBag,
iconRight: Expensicons.NewWindow,
action: () => {
- Link.openExternalLink(CONST.UPWORK_URL);
+ openExternalLink(CONST.UPWORK_URL);
return Promise.resolve();
},
link: CONST.UPWORK_URL,
@@ -90,7 +90,7 @@ function AboutPage() {
{
translationKey: 'initialSettingsPage.aboutPage.reportABug',
icon: Expensicons.Bug,
- action: waitForNavigate(Report.navigateToConciergeChat),
+ action: waitForNavigate(navigateToConciergeChat),
},
];
@@ -117,7 +117,7 @@ function AboutPage() {
selectable
style={[styles.textLabel, styles.textVersion, styles.alignSelfCenter]}
>
- v{Environment.isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`}
+ v{isInternalTestBuild() ? `${pkg.version} PR:${CONST.PULL_REQUEST_NUMBER}${getFlavor()}` : `${pkg.version}${getFlavor()}`}
),
diff --git a/src/pages/workspace/accounting/MultiConnectionSelectorPage.tsx b/src/pages/workspace/accounting/MultiConnectionSelectorPage.tsx
new file mode 100644
index 0000000000000..e8ecca78c7624
--- /dev/null
+++ b/src/pages/workspace/accounting/MultiConnectionSelectorPage.tsx
@@ -0,0 +1,126 @@
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import MenuItemList from '@components/MenuItemList';
+import ScreenWrapper from '@components/ScreenWrapper';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {getConnectionNameFromRouteParam} from '@libs/AccountingUtils';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import CONST from '@src/CONST';
+import type {ConnectionName} from '@src/types/onyx/Policy';
+import {AccountingContextProvider, useAccountingContext} from './AccountingContext';
+import type {MenuItemData} from './types';
+import {getAccountingIntegrationData} from './utils';
+
+type MultiConnectionSelectorPageProps = WithPolicyConnectionsProps & {
+ route: {
+ params: {
+ connection: ValueOf;
+ integrationToDisconnect?: ConnectionName;
+ shouldDisconnectIntegrationBeforeConnecting?: boolean;
+ };
+ };
+};
+
+function MultiConnectionSelectorPage({policy, route}: MultiConnectionSelectorPageProps) {
+ const policyID = policy?.id;
+
+ const {canUseNSQS} = usePermissions();
+ const multiConnectionName = getConnectionNameFromRouteParam(route.params.connection);
+ const integrationToDisconnect = route.params.integrationToDisconnect;
+ const shouldDisconnectIntegrationBeforeConnecting = route.params.shouldDisconnectIntegrationBeforeConnecting;
+
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const {startIntegrationFlow, popoverAnchorRefs} = useAccountingContext();
+
+ const integrations = useMemo(() => CONST.POLICY.CONNECTIONS.MULTI_CONNECTIONS_MAPPING_INVERTED[multiConnectionName] ?? [], [multiConnectionName]);
+
+ const connectionsMenuItems: MenuItemData[] = useMemo(
+ () =>
+ policyID
+ ? (integrations
+ .map((integration) => {
+ const integrationData = getAccountingIntegrationData(integration, policyID, translate);
+ if (!integrationData) {
+ return undefined;
+ }
+
+ const connectionsMenuItem: MenuItemData = {
+ title: integrationData.title,
+ icon: integrationData.icon,
+ iconType: CONST.ICON_TYPE_AVATAR,
+ shouldShowRightIcon: true,
+ onPress: () => {
+ startIntegrationFlow({
+ name: integration,
+ integrationToDisconnect,
+ shouldDisconnectIntegrationBeforeConnecting,
+ });
+ },
+ ref: (ref) => {
+ if (!popoverAnchorRefs?.current) {
+ return;
+ }
+
+ // eslint-disable-next-line react-compiler/react-compiler
+ popoverAnchorRefs.current[integration].current = ref;
+ },
+ };
+
+ return connectionsMenuItem;
+ })
+ .filter(Boolean) as MenuItemData[])
+ : [],
+ [integrations, integrationToDisconnect, shouldDisconnectIntegrationBeforeConnecting, policyID, startIntegrationFlow, popoverAnchorRefs, translate],
+ );
+
+ // The multi connector is currently only used for NSQS (which is behind beta)
+ const shouldBeBlocked = !canUseNSQS || !connectionsMenuItems.length;
+
+ return (
+
+
+
+
+ {translate(`workspace.multiConnectionSelector.description`, {connectionName: multiConnectionName})}
+
+
+
+
+ );
+}
+
+function MultiConnectionSelectorPageeWrapper(props: MultiConnectionSelectorPageProps) {
+ return (
+
+
+
+ );
+}
+
+MultiConnectionSelectorPage.displayName = 'MultiConnectionSelectorPage';
+
+export default withPolicyConnections(MultiConnectionSelectorPageeWrapper);
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index 2f38e66b1bed4..e20239e96ac30 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -28,10 +28,10 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import {getRouteParamForConnection} from '@libs/AccountingUtils';
import {isAuthenticationError, isConnectionInProgress, isConnectionUnverified, removePolicyConnection, syncConnection} from '@libs/actions/connections';
import {getAssignedSupportData} from '@libs/actions/Policy/Policy';
import {getConciergeReportID} from '@libs/actions/Report';
-import * as PolicyUtils from '@libs/PolicyUtils';
import {
areSettingsInErrorFields,
findCurrentXeroOrganization,
@@ -40,14 +40,16 @@ import {
getCurrentXeroOrganizationName,
getIntegrationLastSuccessfulDate,
getXeroTenants,
+ hasUnsupportedIntegration,
isControlPolicy,
settingsPendingAction,
+ shouldShowSyncError,
} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import type {AnchorPosition} from '@styles/index';
-import * as Link from '@userActions/Link';
+import {openOldDotLink} from '@userActions/Link';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -65,12 +67,12 @@ type RouteParams = {
function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`);
- const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${policy?.workspaceAccountID ?? -1}`);
+ const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID}`);
const theme = useTheme();
const styles = useThemeStyles();
const {translate, datetimeToRelative: getDatetimeToRelative} = useLocalize();
const {isOffline} = useNetwork();
- const {canUseNetSuiteUSATax} = usePermissions();
+ const {canUseNetSuiteUSATax, canUseNSQS} = usePermissions();
const {windowWidth} = useWindowDimensions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0});
@@ -89,7 +91,12 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy);
const connectionNames = CONST.POLICY.CONNECTIONS.NAME;
- const accountingIntegrations = Object.values(connectionNames);
+ const accountingIntegrations = Object.values(connectionNames).filter((integration) => {
+ if (integration === CONST.POLICY.CONNECTIONS.NAME.NSQS && !canUseNSQS) {
+ return false;
+ }
+ return true;
+ });
const connectedIntegration = getConnectedIntegration(policy, accountingIntegrations) ?? connectionSyncProgress?.connectionName;
const synchronizationError = connectedIntegration && getSynchronizationErrorMessage(policy, connectedIntegration, isSyncInProgress, translate, styles);
@@ -102,8 +109,8 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
connectedIntegration === connectionSyncProgress?.connectionName ? connectionSyncProgress : undefined,
);
- const hasSyncError = PolicyUtils.shouldShowSyncError(policy, isSyncInProgress);
- const hasUnsupportedNDIntegration = !isEmptyObject(policy?.connections) && PolicyUtils.hasUnsupportedIntegration(policy, accountingIntegrations);
+ const hasSyncError = shouldShowSyncError(policy, isSyncInProgress);
+ const hasUnsupportedNDIntegration = !isEmptyObject(policy?.connections) && hasUnsupportedIntegration(policy, accountingIntegrations);
const tenants = useMemo(() => getXeroTenants(policy), [policy]);
const currentXeroOrganization = findCurrentXeroOrganization(tenants, policy?.connections?.xero?.config?.tenantID);
@@ -262,6 +269,15 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
if (!integrationData) {
return undefined;
}
+
+ const designatedDisplayConnection = CONST.POLICY.CONNECTIONS.MULTI_CONNECTIONS_MAPPING[integration];
+
+ // The multi connector is currently only used for NSQS (which is behind beta)
+ const shouldUseMultiConnectionSelector = !!canUseNSQS && !!designatedDisplayConnection;
+ if (shouldUseMultiConnectionSelector && designatedDisplayConnection !== integration) {
+ return;
+ }
+
const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {};
return {
@@ -272,7 +288,15 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
title: integrationData?.title,
rightComponent: (