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 f9229d5185b4..cc5843603be6 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1569,6 +1569,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 0a21cb17df7e..506f969b9ad1 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -349,6 +349,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 */
@@ -545,6 +547,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 f5d653abe5a7..362ff20d0cc7 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -553,6 +553,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 6c9262deeeb3..e7cd76a1907b 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -213,6 +213,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 920abf3d08f0..6b08dd74dc8b 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 ab1a53a55da9..a1a7d96d5c46 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2,6 +2,7 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import Str from 'expensify-common/lib/str';
import CONST from '@src/CONST';
import type {Country} from '@src/CONST';
+import type {PolicyConnectionSyncStage} from '@src/types/onyx/Policy';
import type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -1822,6 +1823,7 @@ export default {
invoices: 'Invoices',
travel: 'Travel',
members: 'Members',
+ accounting: 'Accounting',
plan: 'Plan',
profile: 'Profile',
bankAccount: 'Bank account',
@@ -2046,6 +2048,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: PolicyConnectionSyncStage) => {
+ 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 6330ade811ca..5ea339d4fe36 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1,5 +1,6 @@
import Str from 'expensify-common/lib/str';
import CONST from '@src/CONST';
+import type {PolicyConnectionSyncStage} from '@src/types/onyx/Policy';
import type {
AddressLineParams,
AdminCanceledRequestParams,
@@ -1849,6 +1850,7 @@ export default {
invoices: 'Enviar facturas',
travel: 'Viajes',
members: 'Miembros',
+ accounting: 'Contabilidad',
plan: 'Plan',
profile: 'Perfil',
bankAccount: 'Cuenta bancaria',
@@ -2041,6 +2043,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: PolicyConnectionSyncStage) => {
+ 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/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 b62a972a864c..2c1598e385bf 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -604,6 +604,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/getQuickBooksOnlineSetupLink.ts b/src/libs/actions/connections/getQuickBooksOnlineSetupLink.ts
new file mode 100644
index 000000000000..a93ebbdae9b2
--- /dev/null
+++ b/src/libs/actions/connections/getQuickBooksOnlineSetupLink.ts
@@ -0,0 +1,13 @@
+import {getCommandURL} from '@libs/ApiUtils';
+import CONFIG from '@src/CONFIG';
+import ROUTES from '@src/ROUTES';
+
+function getQuickBooksOnlineSetupLink(policyID: string) {
+ const callbackPath = `${CONFIG.EXPENSIFY.NEW_EXPENSIFY_URL}${ROUTES.WORKSPACE_ACCOUNTING.getRoute(policyID)}`;
+ const params = new URLSearchParams({callbackPath, policyID});
+ const commandURL = getCommandURL({command: 'ConnectToQuickbooksOnline'});
+
+ return commandURL + params.toString();
+}
+
+export default getQuickBooksOnlineSetupLink;
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index 6d67b4549f29..2e9094f565de 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -14,6 +14,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
+import usePermissions from '@hooks/usePermissions';
import usePrevious from '@hooks/usePrevious';
import useSingleExecution from '@hooks/useSingleExecution';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -69,6 +70,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
const {singleExecution, isExecuting} = useSingleExecution();
const activeRoute = useNavigationState(getTopmostWorkspacesCentralPaneName);
const {translate} = useLocalize();
+ const {canUseAccountingIntegrations} = usePermissions();
const policyID = policy?.id ?? '';
const policyName = policy?.name ?? '';
@@ -198,6 +200,17 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r
});
}
+ if (policy?.areConnectionsEnabled && canUseAccountingIntegrations) {
+ 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..2c87e8803be6
--- /dev/null
+++ b/src/pages/workspace/accounting/WorkspaceAccountingPage.tsx
@@ -0,0 +1,238 @@
+import React, {useCallback, useMemo, useRef, useState} from 'react';
+import {ActivityIndicator, View} from 'react-native';
+// import ConnectToQuickbooksOnlineButton from './qboConnectionButton';
+import Button from '@components/Button';
+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 useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+// import useWaitForNavigation from '@hooks/useWaitForNavigation';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
+import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
+import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
+import withPolicy from '@pages/workspace/withPolicy';
+import type {WithPolicyProps} from '@pages/workspace/withPolicy';
+import type {AnchorPosition} from '@styles/index';
+import CONST from '@src/CONST';
+
+function WorkspaceAccountingPage({policy}: WithPolicyProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ // const {environmentURL} = useEnvironment();
+ // const waitForNavigate = useWaitForNavigation();
+ const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
+
+ const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0});
+
+ // TODO
+ const [policyIsConnectedToAccountingSystem, setPolicyIsConnectedToAccountingSystem] = useState(false);
+
+ // TODO
+ const [isSyncInProgress, setIsSyncInProgress] = useState(false);
+
+ const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
+ const threeDotsMenuContainerRef = useRef(null);
+
+ const policyID = policy?.id ?? '';
+
+ // TODO remove
+ // 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: (
+ // TODO use ConnectToQuickbooksOnlineButton instead
+ //
+
+
+ ),
+ },
+ ],
+ [styles.sectionMenuItemTopDescription, translate, openQBOsync, styles.justifyContentCenter],
+ );
+
+ 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', '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 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..e499e5dd2e0e
--- /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/getQuickBooksOnlineSetupLink';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {ConnectToQuickbooksOnlineButtonOnyxProps, ConnectToQuickbooksOnlineButtonProps} from './types';
+
+type WebViewNavigationEvent = WebViewNavigation;
+
+function ConnectToQuickbooksOnlineButton({policyID, session}: ConnectToQuickbooksOnlineButtonProps) {
+ 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 (
+ <>
+