Skip to content

Commit

Permalink
Merge pull request Expensify#48001 from software-mansion-labs/@szymcz…
Browse files Browse the repository at this point in the history
…ak/sage-intacct-wrong-credentials-handling

Add error handling when user gives wrong credentials.
  • Loading branch information
yuwenmemon authored Sep 4, 2024
2 parents 62e6f63 + 3a7bd63 commit 65c8db5
Show file tree
Hide file tree
Showing 14 changed files with 78 additions and 24 deletions.
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,8 @@ const CONST = {
EXPENSIFY_PACKAGE_FOR_SAGE_INTACCT_FILE_NAME: 'ExpensifyPackageForSageIntacct',
SAGE_INTACCT_INSTRUCTIONS: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct',
HOW_TO_CONNECT_TO_SAGE_INTACCT: 'https://help.expensify.com/articles/expensify-classic/integrations/accounting-integrations/Sage-Intacct#how-to-connect-to-sage-intacct',
SAGE_INTACCT_HELP_LINK:
"https://help.expensify.com/articles/expensify-classic/connections/sage-intacct/Sage-Intacct-Troubleshooting#:~:text=First%20make%20sure%20that%20you,your%20company's%20Web%20Services%20authorizations.",
PRICING: `https://www.expensify.com/pricing`,

// Use Environment.getEnvironmentURL to get the complete URL with port number
Expand Down
Empty file.
Empty file.
Empty file.
Empty file.
9 changes: 9 additions & 0 deletions src/components/ConnectToSageIntacctFlow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import React, {useEffect, useState} from 'react';
import {useOnyx} from 'react-native-onyx';
import * as Expensicons from '@components/Icon/Expensicons';
import PopoverMenu from '@components/PopoverMenu';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import {isAuthenticationError} from '@libs/actions/connections';
import {getAdminPoliciesConnectedToSageIntacct} from '@libs/actions/Policy/Policy';
import Navigation from '@libs/Navigation/Navigation';
import {useAccountingContext} from '@pages/workspace/accounting/AccountingContext';
import type {AnchorPosition} from '@styles/index';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

type ConnectToSageIntacctFlowProps = {
Expand All @@ -23,6 +26,8 @@ function ConnectToSageIntacctFlow({policyID}: ConnectToSageIntacctFlowProps) {
const [reuseConnectionPopoverPosition, setReuseConnectionPopoverPosition] = useState<AnchorPosition>({horizontal: 0, vertical: 0});
const {popoverAnchorRefs} = useAccountingContext();
const threeDotsMenuContainerRef = popoverAnchorRefs?.current?.[CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT];
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
const shouldGoToEnterCredentials = isAuthenticationError(policy, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT);

const connectionOptions = [
{
Expand All @@ -44,6 +49,10 @@ function ConnectToSageIntacctFlow({policyID}: ConnectToSageIntacctFlowProps) {
];

useEffect(() => {
if (shouldGoToEnterCredentials) {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ENTER_CREDENTIALS.getRoute(policyID));
return;
}
if (!hasPoliciesConnectedToSageIntacct) {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES.getRoute(policyID));
return;
Expand Down
2 changes: 1 addition & 1 deletion src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ type MenuItemBaseProps = {
shouldShowDescriptionOnTop?: boolean;

/** Error to display at the bottom of the component */
errorText?: string;
errorText?: string | ReactNode;

/** Any additional styles to pass to error text. */
errorTextStyle?: StyleProp<ViewStyle>;
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2446,6 +2446,8 @@ export default {
syncReimbursedReports: 'Sync reimbursed reports',
syncReimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Sage Intacct account below.',
paymentAccount: 'Sage Intacct payment account',
authenticationError: 'Can’t connect to Sage Intacct due to an authentication error. ',
learnMore: 'Learn more.',
},
netsuite: {
subsidiary: 'Subsidiary',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2490,6 +2490,8 @@ export default {
syncReimbursedReportsDescription:
'Cuando un informe se reembolsa utilizando Expensify ACH, la factura de compra correspondiente se creará en la cuenta de Sage Intacct a continuación.',
paymentAccount: 'Cuenta de pago Sage Intacct',
authenticationError: 'No se puede conectar a Sage Intacct debido a un error de autenticación. ',
learnMore: 'Más información.',
},
netsuite: {
subsidiary: 'Subsidiaria',
Expand Down
4 changes: 2 additions & 2 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
} from '@src/types/onyx/Policy';
import type PolicyEmployee from '@src/types/onyx/PolicyEmployee';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {getSynchronizationErrorMessage} from './actions/connections';
import {hasSynchronizationErrorMessage} from './actions/connections';
import * as Localize from './Localize';
import Navigation from './Navigation/Navigation';
import * as NetworkStore from './Network/NetworkStore';
Expand Down Expand Up @@ -90,7 +90,7 @@ function hasPolicyCategoriesError(policyCategories: OnyxEntry<PolicyCategories>)
* Checks if the policy had a sync error.
*/
function hasSyncError(policy: OnyxEntry<Policy>, isSyncInProgress: boolean): boolean {
return (Object.keys(policy?.connections ?? {}) as ConnectionName[]).some((connection) => !!getSynchronizationErrorMessage(policy, connection, isSyncInProgress));
return (Object.keys(policy?.connections ?? {}) as ConnectionName[]).some((connection) => !!hasSynchronizationErrorMessage(policy, connection, isSyncInProgress));
}

/**
Expand Down
14 changes: 6 additions & 8 deletions src/libs/actions/connections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import * as API from '@libs/API';
import type {RemovePolicyConnectionParams, UpdateManyPolicyConnectionConfigurationsParams, UpdatePolicyConnectionConfigParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import * as Localize from '@libs/Localize';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
Expand Down Expand Up @@ -370,25 +369,24 @@ function updateManyPolicyConnectionConfigs<TConnectionName extends ConnectionNam
API.write(WRITE_COMMANDS.UPDATE_MANY_POLICY_CONNECTION_CONFIGS, parameters, {optimisticData, failureData, successData});
}

function getSynchronizationErrorMessage(policy: OnyxEntry<Policy>, connectionName: PolicyConnectionName, isSyncInProgress: boolean): string | undefined {
const syncError = Localize.translateLocal('workspace.accounting.syncError', connectionName);
function hasSynchronizationErrorMessage(policy: OnyxEntry<Policy>, connectionName: PolicyConnectionName, isSyncInProgress: boolean): boolean {
// NetSuite does not use the conventional lastSync object, so we need to check for lastErrorSyncDate
if (connectionName === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) {
if (
!isSyncInProgress &&
(!!policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.NETSUITE].lastErrorSyncDate || policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]?.verified === false)
) {
return syncError;
return true;
}
return;
return false;
}

const connection = policy?.connections?.[connectionName];

if (isSyncInProgress || isEmptyObject(connection?.lastSync) || connection?.lastSync?.isSuccessful !== false || !connection?.lastSync?.errorDate) {
return;
return false;
}
return `${syncError} ("${connection?.lastSync?.errorMessage}")`;
return true;
}

function isAuthenticationError(policy: OnyxEntry<Policy>, connectionName: PolicyConnectionName) {
Expand Down Expand Up @@ -469,10 +467,10 @@ export {
updatePolicyConnectionConfig,
updatePolicyXeroConnectionConfig,
updateManyPolicyConnectionConfigs,
getSynchronizationErrorMessage,
isAuthenticationError,
syncConnection,
copyExistingPolicyConnection,
isConnectionUnverified,
isConnectionInProgress,
hasSynchronizationErrorMessage,
};
14 changes: 5 additions & 9 deletions src/pages/workspace/accounting/PolicyAccountingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {getSynchronizationErrorMessage, isAuthenticationError, isConnectionInProgress, isConnectionUnverified, removePolicyConnection, syncConnection} from '@libs/actions/connections';
import {isAuthenticationError, isConnectionInProgress, isConnectionUnverified, removePolicyConnection, syncConnection} from '@libs/actions/connections';
import {
areSettingsInErrorFields,
findCurrentXeroOrganization,
Expand All @@ -43,7 +43,7 @@ import ROUTES from '@src/ROUTES';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {AccountingContextProvider, useAccountingContext} from './AccountingContext';
import type {MenuItemData, PolicyAccountingPageProps} from './types';
import {getAccountingIntegrationData} from './utils';
import {getAccountingIntegrationData, getSynchronizationErrorMessage} from './utils';

function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy.id}`);
Expand All @@ -65,15 +65,11 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {

const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME);
const connectedIntegration = getConnectedIntegration(policy, accountingIntegrations) ?? connectionSyncProgress?.connectionName;
const synchronizationError = connectedIntegration && getSynchronizationErrorMessage(policy, connectedIntegration, isSyncInProgress);
const synchronizationError = connectedIntegration && getSynchronizationErrorMessage(policy, connectedIntegration, isSyncInProgress, translate, styles);

// Enter credentials item shouldn't be shown for SageIntacct and NetSuite integrations
const shouldShowEnterCredentials =
connectedIntegration &&
!!synchronizationError &&
isAuthenticationError(policy, connectedIntegration) &&
connectedIntegration !== CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT &&
connectedIntegration !== CONST.POLICY.CONNECTIONS.NAME.NETSUITE;
connectedIntegration && !!synchronizationError && isAuthenticationError(policy, connectedIntegration) && connectedIntegration !== CONST.POLICY.CONNECTIONS.NAME.NETSUITE;

const policyID = policy?.id ?? '-1';
// Get the last successful date of the integration. Then, if `connectionSyncProgress` is the same integration displayed and the state is 'jobDone', get the more recent update time of the two.
Expand All @@ -96,7 +92,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
shouldCallAfterModalHide: true,
disabled: isOffline,
iconRight: Expensicons.NewWindow,
shouldShowRightIcon: true,
shouldShowRightIcon: connectedIntegration !== CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT,
},
]
: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import INPUT_IDS from '@src/types/form/SageIntactCredentialsForm';

Expand Down Expand Up @@ -58,7 +57,7 @@ function EnterSageIntacctCredentialsPage({route}: SageIntacctPrerequisitesPagePr
>
<HeaderWithBackButton
title={translate('workspace.intacct.sageIntacctSetup')}
onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREREQUISITES.getRoute(policyID))}
onBackButtonPress={() => Navigation.goBack()}
/>
<FormProvider
style={[styles.flexGrow1, styles.ph5]}
Expand Down
50 changes: 48 additions & 2 deletions src/pages/workspace/accounting/utils.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import ConnectToNetSuiteFlow from '@components/ConnectToNetSuiteFlow';
import ConnectToQuickbooksOnlineFlow from '@components/ConnectToQuickbooksOnlineFlow';
import ConnectToSageIntacctFlow from '@components/ConnectToSageIntacctFlow';
import ConnectToXeroFlow from '@components/ConnectToXeroFlow';
import * as Expensicons from '@components/Icon/Expensicons';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import {isAuthenticationError} from '@libs/actions/connections';
import * as Localize from '@libs/Localize';
import {canUseTaxNetSuite} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {ThemeStyles} from '@styles/index';
import {getTrackingCategories} from '@userActions/connections/Xero';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Policy} from '@src/types/onyx';
import type {PolicyConnectionName} from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {
getImportCustomFieldsSettings,
shouldHideCustomFormIDOptions,
Expand Down Expand Up @@ -206,5 +213,44 @@ function getAccountingIntegrationData(
}
}

// eslint-disable-next-line import/prefer-default-export
export {getAccountingIntegrationData};
function getSynchronizationErrorMessage(
policy: OnyxEntry<Policy>,
connectionName: PolicyConnectionName,
isSyncInProgress: boolean,
translate: LocaleContextProps['translate'],
styles?: ThemeStyles,
): React.ReactNode | undefined {
const syncError = Localize.translateLocal('workspace.accounting.syncError', connectionName);
// NetSuite does not use the conventional lastSync object, so we need to check for lastErrorSyncDate
if (connectionName === CONST.POLICY.CONNECTIONS.NAME.NETSUITE) {
if (
!isSyncInProgress &&
(!!policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.NETSUITE].lastErrorSyncDate || policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]?.verified === false)
) {
return syncError;
}
return;
}

const connection = policy?.connections?.[connectionName];
if (isSyncInProgress || isEmptyObject(connection?.lastSync) || connection?.lastSync?.isSuccessful !== false || !connection?.lastSync?.errorDate) {
return;
}

if (connectionName === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT && isAuthenticationError(policy, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT)) {
return (
<Text style={[styles?.formError]}>
<Text style={[styles?.formError]}>{translate('workspace.sageIntacct.authenticationError')}</Text>
<TextLink
style={[styles?.link, styles?.fontSizeLabel]}
href={CONST.SAGE_INTACCT_HELP_LINK}
>
{translate('workspace.sageIntacct.learnMore')}
</TextLink>
</Text>
);
}
return `${syncError} ("${connection?.lastSync?.errorMessage}")`;
}

export {getAccountingIntegrationData, getSynchronizationErrorMessage};

0 comments on commit 65c8db5

Please sign in to comment.