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 000000000000..5b8ddb542cf7
--- /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 1250092cb910..19bc28d61d51 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 54b7da704cd1..1fb84c3dd9cf 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 393085ab4384..87664b718974 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 04bb3c6297ba..4ee20f34cf16 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 000000000000..87b32007f9f5
--- /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 000000000000..7a19bd321b99
--- /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 7957896d4006..1bf3712c0f01 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 9a232e83fb97..c7bc37e38e3e 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 5cfa87d472da..da402f612a2c 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 40ec431ca893..32f9f7d5a827 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 b2d79b6243ac..21fd73e7353d 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 020796085ba4..59532361e42d 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 581857ba2ac9..76ae8aeac316 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 000000000000..16a7b93b116d
--- /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 000000000000..319ccb2f1d50
--- /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 000000000000..3712e782e143
--- /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 000000000000..eda70db5027b
--- /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 000000000000..2d4dfaa28862
--- /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 000000000000..56aaca69472b
--- /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 000000000000..73ef027d180b
--- /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 000000000000..ee1be53150ab
--- /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 f6c547a45511..5805c90c4b32 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 1e2a08795562..616d1f1caac1 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 7516048241d6..6f1023a1c136 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 dae1f68d9082..e3b5524f8bba 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 7865993d08e9..79aed5bcf756 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 81e0bb4889c8..1838b85fcee0 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 2e75b1e36280..f57c4c3f5715 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 a84b437a65f7..b3035681d005 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 35272f6c231c..f848af54646a 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 000000000000..740be3ddb01f
--- /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 9087b9fb00c8..5d1131b06836 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 4b5d06da4b68..b2af2c0dfb2b 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 000000000000..e8ecca78c762
--- /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 2f38e66b1bed..e20239e96ac3 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: (