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 ( + <> +