diff --git a/assets/images/integrationicons/qbo-icon-square.svg b/assets/images/integrationicons/qbo-icon-square.svg
new file mode 100644
index 000000000000..a8ce3468ffbf
--- /dev/null
+++ b/assets/images/integrationicons/qbo-icon-square.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/assets/images/integrationicons/xero-icon-square.svg b/assets/images/integrationicons/xero-icon-square.svg
new file mode 100644
index 000000000000..94b79bb3533d
--- /dev/null
+++ b/assets/images/integrationicons/xero-icon-square.svg
@@ -0,0 +1,32 @@
+
+
+
diff --git a/src/CONST.ts b/src/CONST.ts
index e7358b382f14..8dfd9dbef5b8 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1534,6 +1534,28 @@ const CONST = {
AUTOREPORTING_OFFSET: 'autoReportingOffset',
GENERAL_SETTINGS: 'generalSettings',
},
+ CONNECTIONS: {
+ SYNC_STATUS: {
+ STARTING: 'starting',
+ FINISHED: 'finished',
+ PROGRESS: 'progress',
+ },
+ NAME: {
+ // Here we will add other connections names when we add support for them
+ QBO: 'quickbooksOnline',
+ },
+ SYNC_STAGE_NAME: {
+ STARTING_IMPORT: 'startingImport',
+ QBO_CUSTOMERS: 'quickbooksOnlineImportCustomers',
+ QBO_EMPLOYEES: 'quickbooksOnlineImportEmployees',
+ QBO_ACCOUNTS: 'quickbooksOnlineImportAccounts',
+ QBO_CLASSES: 'quickbooksOnlineImportClasses',
+ QBO_LOCATIONS: 'quickbooksOnlineImportLocations',
+ QBO_PROCESSING: 'quickbooksOnlineImportProcessing',
+ QBO_PAYMENTS: 'quickbooksOnlineSyncBillPayments',
+ QBO_TAX_CODES: 'quickbooksOnlineSyncTaxCodes',
+ },
+ },
},
CUSTOM_UNITS: {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 46b2c5f8055c..d2b0a4a6b5ac 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -346,6 +346,8 @@ const ONYXKEYS = {
/** This is deprecated, but needed for a migration, so we still need to include it here so that it will be initialized in Onyx.init */
DEPRECATED_POLICY_MEMBER_LIST: 'policyMemberList_',
+
+ POLICY_CONNECTION_SYNC_PROGRESS: 'policyConnectionSyncProgress_',
},
/** List of Form ids */
@@ -539,6 +541,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string;
[ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep;
[ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember;
+ [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress;
};
type OnyxValuesMapping = {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 8130c271a2db..e7003981f3b8 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -550,6 +550,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/members',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const,
},
+ WORKSPACE_ACCOUNTING: {
+ route: 'settings/workspaces/:policyID/accounting',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const,
+ },
WORKSPACE_CATEGORIES: {
route: 'settings/workspaces/:policyID/categories',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index cf864fd96b3e..7a35350695eb 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -208,6 +208,7 @@ const SCREENS = {
INVOICES: 'Workspace_Invoices',
TRAVEL: 'Workspace_Travel',
MEMBERS: 'Workspace_Members',
+ ACCOUNTING: 'Workspace_Accounting',
INVITE: 'Workspace_Invite',
INVITE_MESSAGE: 'Workspace_Invite_Message',
CATEGORIES: 'Workspace_Categories',
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index 3b7da9f77287..0e017d42e51b 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -1,6 +1,7 @@
import type {ReactNode} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
import type {Action} from '@hooks/useSingleExecution';
import type {StepCounterParams} from '@src/languages/types';
import type {AnchorPosition} from '@src/styles';
@@ -74,7 +75,7 @@ type HeaderWithBackButtonProps = Partial & {
shouldSetModalVisibility?: boolean;
/** List of menu items for more(three dots) menu */
- threeDotsMenuItems?: ThreeDotsMenuItem[];
+ threeDotsMenuItems?: PopoverMenuItem[];
/** The anchor position of the menu */
threeDotsAnchorPosition?: AnchorPosition;
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 1fcf0d07276c..877e4972a3ec 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -84,6 +84,8 @@ import Hourglass from '@assets/images/hourglass.svg';
import ImageCropCircleMask from '@assets/images/image-crop-circle-mask.svg';
import ImageCropSquareMask from '@assets/images/image-crop-square-mask.svg';
import Info from '@assets/images/info.svg';
+import QBOSquare from '@assets/images/integrationicons/qbo-icon-square.svg';
+import XeroSquare from '@assets/images/integrationicons/xero-icon-square.svg';
import Invoice from '@assets/images/invoice.svg';
import Key from '@assets/images/key.svg';
import Keyboard from '@assets/images/keyboard.svg';
@@ -280,6 +282,7 @@ export {
Plus,
Printer,
Profile,
+ QBOSquare,
QrCode,
QuestionMark,
Receipt,
@@ -308,6 +311,7 @@ export {
Wallet,
Workflows,
Workspace,
+ XeroSquare,
Zoom,
Twitter,
Youtube,
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 1fd1c8ef5a3b..cf77ac7c4fd6 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -208,6 +208,7 @@ function PopoverMenu({
focused={focusedIndex === menuIndex}
displayInDefaultIconColor={item.displayInDefaultIconColor}
shouldShowRightIcon={item.shouldShowRightIcon}
+ iconRight={item.iconRight}
shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon}
label={item.label}
isLabelHoverable={item.isLabelHoverable}
diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx
index 464c72b8581f..f6b1f444a24b 100644
--- a/src/components/ThreeDotsMenu/index.tsx
+++ b/src/components/ThreeDotsMenu/index.tsx
@@ -1,11 +1,9 @@
import React, {useEffect, useRef, useState} from 'react';
-import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
-import type {PopoverMenuItem} from '@components/PopoverMenu';
import PopoverMenu from '@components/PopoverMenu';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
@@ -14,53 +12,15 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Browser from '@libs/Browser';
import CONST from '@src/CONST';
-import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {AnchorPosition} from '@src/styles';
import type {Modal} from '@src/types/onyx';
-import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
-import type IconAsset from '@src/types/utils/IconAsset';
+import type ThreeDotsMenuProps from './types';
type ThreeDotsMenuOnyxProps = {
/** Details about any modals being used */
modal: OnyxEntry;
};
-type ThreeDotsMenuProps = ThreeDotsMenuOnyxProps & {
- /** Tooltip for the popup icon */
- iconTooltip?: TranslationPaths;
-
- /** icon for the popup trigger */
- icon?: IconAsset;
-
- /** Any additional styles to pass to the icon container. */
- iconStyles?: StyleProp;
-
- /** The fill color to pass into the icon. */
- iconFill?: string;
-
- /** Function to call on icon press */
- onIconPress?: () => void;
-
- /** menuItems that'll show up on toggle of the popup menu */
- menuItems: PopoverMenuItem[];
-
- /** The anchor position of the menu */
- anchorPosition: AnchorPosition;
-
- /** The anchor alignment of the menu */
- anchorAlignment?: AnchorAlignment;
-
- /** Whether the popover menu should overlay the current view */
- shouldOverlay?: boolean;
-
- /** Whether the menu is disabled */
- disabled?: boolean;
-
- /** Should we announce the Modal visibility changes? */
- shouldSetModalVisibility?: boolean;
-};
-
function ThreeDotsMenu({
iconTooltip = 'common.more',
icon = Expensicons.ThreeDots,
diff --git a/src/components/ThreeDotsMenu/types.ts b/src/components/ThreeDotsMenu/types.ts
new file mode 100644
index 000000000000..6c3618ffc3ce
--- /dev/null
+++ b/src/components/ThreeDotsMenu/types.ts
@@ -0,0 +1,50 @@
+import type {StyleProp, ViewStyle} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
+import type {TranslationPaths} from '@src/languages/types';
+import type {AnchorPosition} from '@src/styles';
+import type {Modal} from '@src/types/onyx';
+import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type ThreeDotsMenuOnyxProps = {
+ /** Details about any modals being used */
+ modal: OnyxEntry;
+};
+
+type ThreeDotsMenuProps = ThreeDotsMenuOnyxProps & {
+ /** Tooltip for the popup icon */
+ iconTooltip?: TranslationPaths;
+
+ /** icon for the popup trigger */
+ icon?: IconAsset;
+
+ /** Any additional styles to pass to the icon container. */
+ iconStyles?: StyleProp;
+
+ /** The fill color to pass into the icon. */
+ iconFill?: string;
+
+ /** Function to call on icon press */
+ onIconPress?: () => void;
+
+ /** menuItems that'll show up on toggle of the popup menu */
+ menuItems: PopoverMenuItem[];
+
+ /** The anchor position of the menu */
+ anchorPosition: AnchorPosition;
+
+ /** The anchor alignment of the menu */
+ anchorAlignment?: AnchorAlignment;
+
+ /** Whether the popover menu should overlay the current view */
+ shouldOverlay?: boolean;
+
+ /** Whether the menu is disabled */
+ disabled?: boolean;
+
+ /** Should we announce the Modal visibility changes? */
+ shouldSetModalVisibility?: boolean;
+};
+
+export default ThreeDotsMenuProps;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 752d17e37d03..c88aa1aca32f 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -49,6 +49,7 @@ import type {
PayerPaidAmountParams,
PayerPaidParams,
PayerSettledParams,
+ PolicyConnectionSyncStageParams,
RemovedTheRequestParams,
RenamedRoomActionParams,
ReportArchiveReasonsClosedParams,
@@ -1794,6 +1795,7 @@ export default {
invoices: 'Invoices',
travel: 'Travel',
members: 'Members',
+ accounting: 'Accounting',
plan: 'Plan',
profile: 'Profile',
bankAccount: 'Bank account',
@@ -2014,6 +2016,41 @@ export default {
invalidRateError: 'Please enter a valid rate',
lowRateError: 'Rate must be greater than 0',
},
+ accounting: {
+ title: 'Connections',
+ subtitle: 'Connect to your accounting system to code transactions with your chart of accounts, auto-match payments and keep your finances in sync.',
+ qbo: 'Quickbooks Online',
+ xero: 'Xero',
+ setup: 'Set up',
+ lastSync: 'Last synced just now',
+ import: 'Import',
+ export: 'Export',
+ advanced: 'Advanced',
+ other: 'Other integrations',
+ syncNow: 'Sync now',
+ disconnect: 'Disconnect',
+ disconnectTitle: 'Disconnect integration',
+ disconnectPrompt: 'Are you sure you want to disconnect this integration?',
+ enterCredentials: 'Enter your credentials',
+ connections: {
+ syncStageName: ({stage}: PolicyConnectionSyncStageParams) => {
+ switch (stage) {
+ case 'quickbooksOnlineImportCustomers':
+ return 'Importing customers';
+ case 'quickbooksOnlineImportEmployees':
+ return 'Importing employees';
+ case 'quickbooksOnlineImportAccounts':
+ return 'Importing accounts';
+ case 'quickbooksOnlineImportClasses':
+ return 'Importing classes';
+
+ default: {
+ return `Translation missing for stage: ${stage}`;
+ }
+ }
+ },
+ },
+ },
bills: {
manageYourBills: 'Manage your bills',
askYourVendorsBeforeEmail: 'Ask your vendors to forward their invoices to ',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 53ee6d3fba79..cf016d23bcef 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -48,6 +48,7 @@ import type {
PayerPaidAmountParams,
PayerPaidParams,
PayerSettledParams,
+ PolicyConnectionSyncStageParams,
RemovedTheRequestParams,
RenamedRoomActionParams,
ReportArchiveReasonsClosedParams,
@@ -1821,6 +1822,7 @@ export default {
invoices: 'Enviar facturas',
travel: 'Viajes',
members: 'Miembros',
+ accounting: 'Contabilidad',
plan: 'Plan',
profile: 'Perfil',
bankAccount: 'Cuenta bancaria',
@@ -2009,6 +2011,41 @@ export default {
invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`,
membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.',
},
+ accounting: {
+ title: 'Conexiones',
+ subtitle: 'Conecta a tu sistema de contabilidad para codificar transacciones con tu plan de cuentas, auto-cotejar pagos y mantener tus finanzas sincronizadas.',
+ qbo: 'Quickbooks Online',
+ xero: 'Xero',
+ setup: 'Configurar',
+ lastSync: 'Recién sincronizado',
+ import: 'Importar',
+ export: 'Exportar',
+ advanced: 'Avanzado',
+ other: 'Otras integraciones',
+ syncNow: 'Sincronizar ahora',
+ disconnect: 'Desconectar',
+ disconnectTitle: 'Desconectar integración',
+ disconnectPrompt: '¿Estás seguro de que deseas desconectar esta intregración?',
+ enterCredentials: 'Ingresa tus credenciales',
+ connections: {
+ syncStageName: ({stage}: PolicyConnectionSyncStageParams) => {
+ switch (stage) {
+ case 'quickbooksOnlineImportCustomers':
+ return 'Importando clientes';
+ case 'quickbooksOnlineImportEmployees':
+ return 'Importing employees';
+ case 'quickbooksOnlineImportAccounts':
+ return 'Importing accounts';
+ case 'quickbooksOnlineImportClasses':
+ return 'Importing classes';
+
+ default: {
+ return `Translation missing for stage: ${stage}`;
+ }
+ }
+ },
+ },
+ },
card: {
header: 'Desbloquea Tarjetas Expensify gratis',
headerWithEcard: '¡Tus tarjetas están listas!',
diff --git a/src/languages/types.ts b/src/languages/types.ts
index c365363f84af..403fed80a6a8 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -1,4 +1,5 @@
import type {ReportAction} from '@src/types/onyx';
+import type {PolicyConnectionSyncStage} from '@src/types/onyx/Policy';
import type en from './en';
type AddressLineParams = {
@@ -297,6 +298,8 @@ type HeldRequestParams = {comment: string};
type DistanceRateOperationsParams = {count: number};
+type PolicyConnectionSyncStageParams = {stage: PolicyConnectionSyncStage};
+
export type {
AdminCanceledRequestParams,
ApprovedAmountParams,
@@ -400,4 +403,5 @@ export type {
ZipCodeExampleFormatParams,
LogSizeParams,
HeldRequestParams,
+ PolicyConnectionSyncStageParams,
};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx
index 14153809bc86..2dce4247c7ae 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/WorkspaceSettingsModalStackNavigator.tsx
@@ -50,6 +50,13 @@ function WorkspaceSettingsModalStackNavigator() {
name={SCREENS.WORKSPACE.MEMBERS}
getComponent={() => require('@pages/workspace/WorkspaceMembersPage').default as React.ComponentType}
/>
+
+ require('@pages/workspace/accounting/WorkspaceAccountingPage').default as React.ComponentType}
+ />
+
['config'] = {
[SCREENS.WORKSPACE.MEMBERS]: {
path: ROUTES.WORKSPACE_MEMBERS.route,
},
+ [SCREENS.WORKSPACE.ACCOUNTING]: {
+ path: ROUTES.WORKSPACE_ACCOUNTING.route,
+ },
[SCREENS.WORKSPACE.CATEGORIES]: {
path: ROUTES.WORKSPACE_CATEGORIES.route,
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index b88c44b9aa70..75badf2ef5c0 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -609,6 +609,9 @@ type WorkspacesCentralPaneNavigatorParamList = {
[SCREENS.WORKSPACE.MEMBERS]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.ACCOUNTING]: {
+ policyID: string;
+ };
[SCREENS.WORKSPACE.CATEGORIES]: {
policyID: string;
};
diff --git a/src/libs/actions/connections/QuickBooksOnline.ts b/src/libs/actions/connections/QuickBooksOnline.ts
new file mode 100644
index 000000000000..a55cc74e9fac
--- /dev/null
+++ b/src/libs/actions/connections/QuickBooksOnline.ts
@@ -0,0 +1,12 @@
+import {getCommandURL} from '@libs/ApiUtils';
+import CONFIG from '@src/CONFIG';
+
+const getQuickBooksOnlineSetupLink = (policyID: string) => {
+ const callbackPath = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}/workspace/${policyID}/accounting`;
+ const otherParams = new URLSearchParams({callbackPath, policyID}).toString();
+ const commandURL = getCommandURL({command: 'ConnectToQuickbooksOnline'});
+
+ return `${commandURL}&${otherParams}`;
+};
+
+export default getQuickBooksOnlineSetupLink;
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 0d3bb027b8e6..139015fc0496 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -198,6 +198,17 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
});
}
+ if (policy?.areAccountingEnabled) {
+ protectedCollectPolicyMenuItems.push({
+ translationKey: 'workspace.common.accounting',
+ icon: Expensicons.Sync,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID)))),
+ // brickRoadIndicator should be set when API will be ready
+ brickRoadIndicator: undefined,
+ routeName: SCREENS.WORKSPACE.ACCOUNTING,
+ });
+ }
+
protectedCollectPolicyMenuItems.push({
translationKey: 'workspace.common.moreFeatures',
icon: Expensicons.Gear,
diff --git a/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx b/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx
new file mode 100644
index 000000000000..ce5efe033e2f
--- /dev/null
+++ b/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx
@@ -0,0 +1,249 @@
+import React, {useMemo, useRef, useState} from 'react';
+import {ActivityIndicator, View} from 'react-native';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import ConfirmModal from '@components/ConfirmModal';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import * as Illustrations from '@components/Icon/Illustrations';
+import type {MenuItemProps} from '@components/MenuItem';
+import MenuItemList from '@components/MenuItemList';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import Section from '@components/Section';
+import ThreeDotsMenu from '@components/ThreeDotsMenu';
+import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types';
+import useEnvironment from '@hooks/useEnvironment';
+import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+// import useWaitForNavigation from '@hooks/useWaitForNavigation';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import withPolicy from '@pages/workspace/withPolicy';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import type {AnchorPosition} from '@styles/index';
+import CONST from '@src/CONST';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import ConnectToQuickbooksOnlineButton from './qboConnectionButton';
+
+function WorkspaceAccountingPage({policy}: WithPolicyProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {environmentURL} = useEnvironment();
+ // const waitForNavigate = useWaitForNavigation();
+ const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
+ const {canUseAccountingIntegrations} = usePermissions();
+ const hasAccess = PolicyUtils.isPolicyAdmin(policy) && policy?.areConnectionsEnabled && canUseAccountingIntegrations;
+
+ const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0});
+
+ // TODO
+ // const [policyIsConnectedToAccountingSystem, setPolicyIsConnectedToAccountingSystem] = useState(false);
+ const [policyIsConnectedToAccountingSystem] = useState(false);
+
+ // TODO
+ // const [isSyncInProgress, setIsSyncInProgress] = useState(false);
+ const [isSyncInProgress] = useState(false);
+
+ const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
+ const threeDotsMenuContainerRef = useRef(null);
+
+ const policyID = policy?.id ?? '';
+
+ // TODO
+ // fake a QBO connection sync
+ // const openQBOsync = useCallback(() => {
+ // setIsSyncInProgress(true);
+ // setTimeout(() => setIsSyncInProgress(false), 5000);
+ // setPolicyIsConnectedToAccountingSystem(true);
+ // }, []);
+
+ const connectionsMenuItems: MenuItemProps[] = useMemo(
+ () => [
+ {
+ icon: Expensicons.QBOSquare,
+ iconType: 'avatar',
+ interactive: false,
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ shouldShowRightComponent: true,
+ title: translate('workspace.accounting.qbo'),
+ rightComponent: (
+
+ ),
+ },
+ ],
+ [styles.sectionMenuItemTopDescription, translate, policyID, environmentURL],
+ );
+
+ const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo(
+ () => [
+ {
+ icon: Expensicons.Sync,
+ text: translate('workspace.accounting.syncNow'),
+ onSelected: () => {},
+ },
+ {
+ icon: Expensicons.Trashcan,
+ text: translate('workspace.accounting.disconnect'),
+ onSelected: () => setIsDisconnectModalOpen(true),
+ },
+ ],
+ [translate],
+ );
+
+ const qboConnectionMenuItems: MenuItemProps[] = useMemo(
+ () => [
+ {
+ icon: Expensicons.QBOSquare,
+ iconType: 'avatar',
+ interactive: false,
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ shouldShowRightComponent: true,
+ title: translate('workspace.accounting.qbo'),
+ description: isSyncInProgress
+ ? translate('workspace.accounting.connections.syncStageName', {stage: 'quickbooksOnlineImportCustomers'})
+ : translate('workspace.accounting.lastSync'),
+ rightComponent: isSyncInProgress ? (
+
+ ) : (
+
+ {
+ threeDotsMenuContainerRef.current?.measureInWindow((x, y, width, height) => {
+ setThreeDotsMenuPosition({
+ horizontal: x + width,
+ vertical: y + height,
+ });
+ });
+ }}
+ menuItems={overflowMenu}
+ anchorPosition={threeDotsMenuPosition}
+ anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
+ />
+
+ ),
+ },
+ ...(isSyncInProgress
+ ? []
+ : [
+ {
+ icon: Expensicons.Pencil,
+ iconRight: Expensicons.ArrowRight,
+ shouldShowRightIcon: true,
+ title: translate('workspace.accounting.import'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ onPress: () => {},
+ },
+ {
+ icon: Expensicons.Send,
+ iconRight: Expensicons.ArrowRight,
+ shouldShowRightIcon: true,
+ title: translate('workspace.accounting.export'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ onPress: () => {},
+ },
+ {
+ icon: Expensicons.Gear,
+ iconRight: Expensicons.ArrowRight,
+ shouldShowRightIcon: true,
+ title: translate('workspace.accounting.advanced'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ onPress: () => {},
+ },
+ ]),
+ ],
+ [translate, theme.spinner, isSyncInProgress, overflowMenu, threeDotsMenuPosition, styles.popoverMenuIcon, threeDotsMenuContainerRef, styles.sectionMenuItemTopDescription],
+ );
+
+ const otherConnectionMenuItems: MenuItemProps[] = useMemo(
+ () => [
+ {
+ key: 'workspace.accounting.other',
+ iconRight: Expensicons.DownArrow,
+ shouldShowRightIcon: true,
+ description: translate('workspace.accounting.other'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription, styles.mt3],
+ onPress: () => {},
+ },
+ ],
+ [styles.sectionMenuItemTopDescription, styles.mt3, translate],
+ );
+
+ const headerThreeDotsMenuItems: ThreeDotsMenuProps['menuItems'] = [
+ {
+ icon: Expensicons.Key,
+ shouldShowRightIcon: true,
+ iconRight: Expensicons.NewWindow,
+ text: translate('workspace.accounting.enterCredentials'),
+ onSelected: () => {},
+ },
+ {
+ icon: Expensicons.Trashcan,
+ text: translate('workspace.accounting.disconnect'),
+ onSelected: () => setIsDisconnectModalOpen(true),
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
+ {}}
+ onCancel={() => setIsDisconnectModalOpen(false)}
+ prompt={translate('workspace.accounting.disconnectPrompt')}
+ confirmText={translate('workspace.accounting.disconnect')}
+ cancelText={translate('common.cancel')}
+ danger
+ />
+
+
+ );
+}
+
+WorkspaceAccountingPage.displayName = 'WorkspaceAccountingPage';
+
+export default withPolicy(WorkspaceAccountingPage);
diff --git a/src/pages/workspace/accounting/qboConnectionButton/index.native.tsx b/src/pages/workspace/accounting/qboConnectionButton/index.native.tsx
new file mode 100644
index 000000000000..ce651381f62b
--- /dev/null
+++ b/src/pages/workspace/accounting/qboConnectionButton/index.native.tsx
@@ -0,0 +1,71 @@
+import React, {useCallback, useRef, useState} from 'react';
+import {withOnyx} from 'react-native-onyx';
+import WebView from 'react-native-webview';
+import type {WebViewNavigation} from 'react-native-webview';
+import Button from '@components/Button';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import Modal from '@components/Modal';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import getQuickBooksOnlineSetupLink from '@libs/actions/connections/QuickBooksOnline';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {ConnectToQuickbooksOnlineButtonOnyxProps, ConnectToQuickbooksOnlineButtonPropsWithSession} from './types';
+
+type WebViewNavigationEvent = WebViewNavigation;
+
+function ConnectToQuickbooksOnlineButton({policyID, session}: ConnectToQuickbooksOnlineButtonPropsWithSession) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const webViewRef = useRef(null);
+ const [isWebViewOpen, setWebViewOpen] = useState(false);
+
+ const authToken = session?.authToken ?? null;
+
+ const renderLoading = () => ;
+
+ /**
+ * Handles in-app navigation for webview links
+ */
+ const handleNavigationStateChange = useCallback(({url}: WebViewNavigationEvent) => !!url, []);
+
+ return (
+ <>
+