Skip to content

Commit

Permalink
Merge pull request #45493 from software-mansion-labs/war-in/offline-e…
Browse files Browse the repository at this point in the history
…rrors-remaining-integrations

Offline and errors pattern in Xero integration
  • Loading branch information
yuwenmemon authored Jul 25, 2024
2 parents 9c4de3d + 4e41702 commit 6d2cf34
Show file tree
Hide file tree
Showing 22 changed files with 512 additions and 222 deletions.
8 changes: 8 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,9 +1340,17 @@ const CONST = {

XERO_CONFIG: {
AUTO_SYNC: 'autoSync',
ENABLED: 'enabled',
REIMBURSEMENT_ACCOUNT_ID: 'reimbursementAccountID',
INVOICE_COLLECTIONS_ACCOUNT_ID: 'invoiceCollectionsAccountID',
SYNC: 'sync',
SYNC_REIMBURSED_REPORTS: 'syncReimbursedReports',
ENABLE_NEW_CATEGORIES: 'enableNewCategories',
EXPORT: 'export',
EXPORTER: 'exporter',
BILL_DATE: 'billDate',
BILL_STATUS: 'billStatus',
NON_REIMBURSABLE_ACCOUNT: 'nonReimbursableAccount',
TENANT_ID: 'tenantID',
IMPORT_CUSTOMERS: 'importCustomers',
IMPORT_TAX_RATES: 'importTaxRates',
Expand Down
13 changes: 11 additions & 2 deletions src/components/SelectionScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type SelectionScreenProps<T = string> = {
displayName: string;

/** Title of the selection component */
title: TranslationPaths;
title?: TranslationPaths;

/** Custom content to display in the header */
headerContent?: React.ReactNode;
Expand Down Expand Up @@ -84,6 +84,12 @@ type SelectionScreenProps<T = string> = {

/** A function to run when the X button next to the error is clicked */
onClose?: () => void;

/** Whether to debounce `onRowSelect` */
shouldDebounceRowSelect?: boolean;

/** Used for dynamic header title translation with parameters */
headerTitleAlreadyTranslated?: string;
};

function SelectionScreen<T = string>({
Expand All @@ -106,6 +112,8 @@ function SelectionScreen<T = string>({
errors,
errorRowStyles,
onClose,
shouldDebounceRowSelect,
headerTitleAlreadyTranslated,
}: SelectionScreenProps<T>) {
const {translate} = useLocalize();
const styles = useThemeStyles();
Expand All @@ -125,7 +133,7 @@ function SelectionScreen<T = string>({
testID={displayName}
>
<HeaderWithBackButton
title={translate(title)}
title={headerTitleAlreadyTranslated ?? (title ? translate(title) : '')}
onBackButtonPress={onBackButtonPress}
/>
{headerContent}
Expand All @@ -144,6 +152,7 @@ function SelectionScreen<T = string>({
listEmptyContent={listEmptyContent}
listFooterContent={listFooterContent}
sectionListStyle={[styles.flexGrow0]}
shouldDebounceRowSelect={shouldDebounceRowSelect}
>
<ErrorMessageRow
errors={errors}
Expand Down
27 changes: 25 additions & 2 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {Str} from 'expensify-common';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type {Except, LiteralUnion, ValueOf} from 'type-fest';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import type {SelectorType} from '@components/SelectionScreen';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm';
import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx';
import type {ErrorFields, PendingAction, PendingFields} from '@src/types/onyx/OnyxCommon';
import type {
ConnectionLastSync,
ConnectionName,
Expand Down Expand Up @@ -41,6 +42,8 @@ type ConnectionWithLastSyncData = {
lastSync?: ConnectionLastSync;
};

type XeroSettings = Array<LiteralUnion<ValueOf<Except<typeof CONST.XERO_CONFIG, 'INVOICE_STATUS' | 'TRACKING_CATEGORY_FIELDS' | 'TRACKING_CATEGORY_OPTIONS'>>, string>>;

let allPolicies: OnyxCollection<Policy>;

Onyx.connect({
Expand Down Expand Up @@ -528,6 +531,24 @@ function getXeroBankAccountsWithDefaultSelect(policy: Policy | undefined, select
}));
}

function areXeroSettingsInErrorFields(settings?: XeroSettings, errorFields?: ErrorFields) {
if (settings === undefined || errorFields === undefined) {
return false;
}

const keys = Object.keys(errorFields);
return settings.some((setting) => keys.includes(setting));
}

function xeroSettingsPendingAction(settings?: XeroSettings, pendingFields?: PendingFields<string>): PendingAction | undefined {
if (settings === undefined || pendingFields === undefined) {
return null;
}

const key = Object.keys(pendingFields).find((setting) => settings.includes(setting));
return pendingFields[key ?? '-1'];
}

function getNetSuiteVendorOptions(policy: Policy | undefined, selectedVendorId: string | undefined): SelectorType[] {
const vendors = policy?.connections?.netsuite.options.data.vendors ?? [];

Expand Down Expand Up @@ -883,6 +904,8 @@ export {
getCurrentSageIntacctEntityName,
hasNoPolicyOtherThanPersonalType,
getCurrentTaxID,
areXeroSettingsInErrorFields,
xeroSettingsPendingAction,
};

export type {MemberEmailsToAccountIDs};
export type {MemberEmailsToAccountIDs, XeroSettings};
112 changes: 111 additions & 1 deletion src/libs/actions/connections/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import isObject from 'lodash/isObject';
import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
Expand All @@ -6,6 +7,7 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
import type {ConnectionName, Connections, PolicyConnectionName} from '@src/types/onyx/Policy';
import type Policy from '@src/types/onyx/Policy';

Expand Down Expand Up @@ -36,6 +38,106 @@ function removePolicyConnection(policyID: string, connectionName: PolicyConnecti
API.write(WRITE_COMMANDS.REMOVE_POLICY_CONNECTION, parameters, {optimisticData});
}

function createPendingFields<TConnectionName extends ConnectionNameExceptNetSuite, TSettingName extends keyof Connections[TConnectionName]['config']>(
settingName: TSettingName,
settingValue: Partial<Connections[TConnectionName]['config'][TSettingName]>,
pendingValue: OnyxCommon.PendingAction,
) {
if (!isObject(settingValue)) {
return {[settingName]: pendingValue};
}

return Object.keys(settingValue).reduce<Record<string, OnyxCommon.PendingAction>>((acc, setting) => {
acc[setting] = pendingValue;
return acc;
}, {});
}

function createErrorFields<TConnectionName extends ConnectionNameExceptNetSuite, TSettingName extends keyof Connections[TConnectionName]['config']>(
settingName: TSettingName,
settingValue: Partial<Connections[TConnectionName]['config'][TSettingName]>,
errorValue: OnyxCommon.Errors | null,
) {
if (!isObject(settingValue)) {
return {[settingName]: errorValue};
}

return Object.keys(settingValue).reduce<OnyxCommon.ErrorFields>((acc, setting) => {
acc[setting] = errorValue;
return acc;
}, {});
}

function updatePolicyXeroConnectionConfig<TConnectionName extends ConnectionNameExceptNetSuite, TSettingName extends keyof Connections[TConnectionName]['config']>(
policyID: string,
connectionName: TConnectionName,
settingName: TSettingName,
settingValue: Partial<Connections[TConnectionName]['config'][TSettingName]>,
) {
const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
connections: {
[connectionName]: {
config: {
[settingName]: settingValue ?? null,
pendingFields: createPendingFields(settingName, settingValue, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE),
errorFields: createErrorFields(settingName, settingValue, null),
},
},
},
},
},
];

const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
connections: {
[connectionName]: {
config: {
[settingName]: settingValue ?? null,
pendingFields: createPendingFields(settingName, settingValue, null),
errorFields: createErrorFields(settingName, settingValue, ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')),
},
},
},
},
},
];

const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
connections: {
[connectionName]: {
config: {
[settingName]: settingValue ?? null,
pendingFields: createPendingFields(settingName, settingValue, null),
errorFields: createErrorFields(settingName, settingValue, null),
},
},
},
},
},
];

const parameters: UpdatePolicyConnectionConfigParams = {
policyID,
connectionName,
settingName: String(settingName),
settingValue: JSON.stringify(settingValue),
idempotencyKey: String(settingName),
};
API.write(WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG, parameters, {optimisticData, failureData, successData});
}

function updatePolicyConnectionConfig<TConnectionName extends ConnectionNameExceptNetSuite, TSettingName extends keyof Connections[TConnectionName]['config']>(
policyID: string,
connectionName: TConnectionName,
Expand Down Expand Up @@ -304,4 +406,12 @@ function copyExistingPolicyConnection(connectedPolicyID: string, targetPolicyID:
);
}

export {removePolicyConnection, updatePolicyConnectionConfig, updateManyPolicyConnectionConfigs, hasSynchronizationError, syncConnection, copyExistingPolicyConnection};
export {
removePolicyConnection,
updatePolicyConnectionConfig,
updatePolicyXeroConnectionConfig,
updateManyPolicyConnectionConfigs,
hasSynchronizationError,
syncConnection,
copyExistingPolicyConnection,
};
54 changes: 48 additions & 6 deletions src/pages/workspace/accounting/PolicyAccountingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,23 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {hasSynchronizationError, removePolicyConnection, syncConnection} from '@libs/actions/connections';
import * as PolicyUtils from '@libs/PolicyUtils';
import {findCurrentXeroOrganization, getCurrentSageIntacctEntityName, getCurrentXeroOrganizationName, getIntegrationLastSuccessfulDate, getXeroTenants} from '@libs/PolicyUtils';
import {
areXeroSettingsInErrorFields,
findCurrentXeroOrganization,
getConnectedIntegration,
getCurrentSageIntacctEntityName,
getCurrentXeroOrganizationName,
getIntegrationLastSuccessfulDate,
getXeroTenants,
xeroSettingsPendingAction,
} from '@libs/PolicyUtils';
import type {XeroSettings} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import type {AnchorPosition} from '@styles/index';
import {getTrackingCategories} from '@userActions/connections/ConnectToXero';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand All @@ -63,8 +73,11 @@ type AccountingIntegration = {
icon: IconAsset;
setupConnectionButton: React.ReactNode;
onImportPagePress: () => void;
subscribedImportSettings?: XeroSettings;
onExportPagePress: () => void;
subscribedExportSettings?: XeroSettings;
onAdvancedPagePress: () => void;
subscribedAdvancedSettings?: XeroSettings;
onCardReconciliationPagePress: () => void;
};
function accountingIntegrationData(
Expand All @@ -73,6 +86,7 @@ function accountingIntegrationData(
translate: LocaleContextProps['translate'],
isConnectedToIntegration?: boolean,
integrationToDisconnect?: PolicyConnectionName,
policy?: Policy,
): AccountingIntegration | undefined {
switch (connectionName) {
case CONST.POLICY.CONNECTIONS.NAME.QBO:
Expand Down Expand Up @@ -103,9 +117,23 @@ function accountingIntegrationData(
/>
),
onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.getRoute(policyID)),
subscribedImportSettings: [
CONST.XERO_CONFIG.ENABLE_NEW_CATEGORIES,
CONST.XERO_CONFIG.IMPORT_TRACKING_CATEGORIES,
CONST.XERO_CONFIG.IMPORT_CUSTOMERS,
CONST.XERO_CONFIG.IMPORT_TAX_RATES,
...getTrackingCategories(policy).map((category) => `${CONST.XERO_CONFIG.TRACKING_CATEGORY_PREFIX}${category.id}`),
],
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID)),
subscribedExportSettings: [CONST.XERO_CONFIG.EXPORTER, CONST.XERO_CONFIG.BILL_DATE, CONST.XERO_CONFIG.BILL_STATUS, CONST.XERO_CONFIG.NON_REIMBURSABLE_ACCOUNT],
onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO)),
onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.getRoute(policyID)),
subscribedAdvancedSettings: [
CONST.XERO_CONFIG.ENABLED,
CONST.XERO_CONFIG.SYNC_REIMBURSED_REPORTS,
CONST.XERO_CONFIG.REIMBURSEMENT_ACCOUNT_ID,
CONST.XERO_CONFIG.INVOICE_COLLECTIONS_ACCOUNT_ID,
],
};
case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
return {
Expand Down Expand Up @@ -167,7 +195,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {

const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME).filter((name) => !(name === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT && !canUseSageIntacctIntegration));

const connectedIntegration = PolicyUtils.getConnectedIntegration(policy, accountingIntegrations) ?? connectionSyncProgress?.connectionName;
const connectedIntegration = getConnectedIntegration(policy, accountingIntegrations) ?? connectionSyncProgress?.connectionName;

const policyID = policy?.id ?? '-1';
const successfulDate = getIntegrationLastSuccessfulDate(connectedIntegration ? policy?.connections?.[connectedIntegration] : undefined);
Expand Down Expand Up @@ -219,8 +247,10 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
}
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.getRoute(policyID, currentXeroOrganization?.id ?? '-1'));
},
pendingAction: policy?.connections?.xero?.config?.pendingFields?.tenantID,
brickRoadIndicator: policy?.connections?.xero?.config?.errorFields?.tenantID ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined,
pendingAction: xeroSettingsPendingAction([CONST.XERO_CONFIG.TENANT_ID], policy?.connections?.xero?.config?.pendingFields),
brickRoadIndicator: areXeroSettingsInErrorFields([CONST.XERO_CONFIG.TENANT_ID], policy?.connections?.xero?.config?.errorFields)
? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
: undefined,
};
case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
return {
Expand Down Expand Up @@ -285,7 +315,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
return [];
}
const shouldShowSynchronizationError = hasSynchronizationError(policy, connectedIntegration, isSyncInProgress);
const integrationData = accountingIntegrationData(connectedIntegration, policyID, translate);
const integrationData = accountingIntegrationData(connectedIntegration, policyID, translate, undefined, undefined, policy);
const iconProps = integrationData?.icon ? {icon: integrationData.icon, iconType: CONST.ICON_TYPE_AVATAR} : {};
return [
{
Expand Down Expand Up @@ -334,6 +364,10 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
title: translate('workspace.accounting.import'),
wrapperStyle: [styles.sectionMenuItemTopDescription],
onPress: integrationData?.onImportPagePress,
brickRoadIndicator: areXeroSettingsInErrorFields(integrationData?.subscribedImportSettings, policy?.connections?.xero?.config?.errorFields)
? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
: undefined,
pendingAction: xeroSettingsPendingAction(integrationData?.subscribedImportSettings, policy?.connections?.xero?.config?.pendingFields),
},
{
icon: Expensicons.Send,
Expand All @@ -342,6 +376,10 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
title: translate('workspace.accounting.export'),
wrapperStyle: [styles.sectionMenuItemTopDescription],
onPress: integrationData?.onExportPagePress,
brickRoadIndicator: areXeroSettingsInErrorFields(integrationData?.subscribedExportSettings, policy?.connections?.xero?.config?.errorFields)
? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
: undefined,
pendingAction: xeroSettingsPendingAction(integrationData?.subscribedExportSettings, policy?.connections?.xero?.config?.pendingFields),
},
{
icon: Expensicons.ExpensifyCard,
Expand All @@ -359,6 +397,10 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
title: translate('workspace.accounting.advanced'),
wrapperStyle: [styles.sectionMenuItemTopDescription],
onPress: integrationData?.onAdvancedPagePress,
brickRoadIndicator: areXeroSettingsInErrorFields(integrationData?.subscribedAdvancedSettings, policy?.connections?.xero?.config?.errorFields)
? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR
: undefined,
pendingAction: xeroSettingsPendingAction(integrationData?.subscribedAdvancedSettings, policy?.connections?.xero?.config?.pendingFields),
},
]),
];
Expand Down
Loading

0 comments on commit 6d2cf34

Please sign in to comment.