Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline and errors pattern in Xero integration #45493

Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d11de01
apply offline and errors pattern for xero export
war-in Jul 16, 2024
564d229
update xero advanced page
war-in Jul 16, 2024
d629d70
revert max retries
war-in Jul 16, 2024
e0d0e53
update XeroOrganizationConfigurationPage
war-in Jul 16, 2024
9ed5911
update XeroTrackingCategoryConfigurationPage
war-in Jul 16, 2024
82c04fd
Merge branch 'refs/heads/main' into war-in/offline-errors-remaining-i…
war-in Jul 16, 2024
f2ed76e
add translated title to Selection Screen
war-in Jul 16, 2024
2fee8f5
Merge branch 'refs/heads/main' into war-in/offline-errors-remaining-i…
war-in Jul 17, 2024
5d8ec28
show RBRs everywhere
war-in Jul 17, 2024
a6861e4
fix typescript errors
war-in Jul 17, 2024
25be841
use more specific type
war-in Jul 17, 2024
854b71f
add function for pending action extraction
war-in Jul 17, 2024
e8f6808
Merge branch 'refs/heads/main' into war-in/offline-errors-remaining-i…
war-in Jul 22, 2024
4db4bed
use unified offline flow on advanced page
war-in Jul 22, 2024
c2de3de
use unified offline flow on import page
war-in Jul 22, 2024
b20bbed
use unified offline flow on export page
war-in Jul 22, 2024
73cc0c2
fix lint
war-in Jul 22, 2024
d8f6cd9
simplify PolicyAccountingPage
war-in Jul 22, 2024
b6bda66
revert retries
war-in Jul 22, 2024
862e0af
Merge branch 'refs/heads/main' into war-in/offline-errors-remaining-i…
war-in Jul 22, 2024
e880a1d
use default tracking categories options value
war-in Jul 22, 2024
147dea1
fix lint error
war-in Jul 23, 2024
ae2b0d9
Merge branch 'refs/heads/main' into war-in/offline-errors-remaining-i…
war-in Jul 24, 2024
0d19456
set `pendingFields` to `null` after failed request
war-in Jul 24, 2024
4ad177b
do not change original `updatePolicyConnectionConfig`
war-in Jul 25, 2024
4e41702
do not change original `updatePolicyConnectionConfig`
war-in Jul 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1335,9 +1335,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 @@ -520,6 +523,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 @@ -869,6 +890,8 @@ export {
isNetSuiteCustomFieldPropertyEditable,
getCurrentSageIntacctEntityName,
hasNoPolicyOtherThanPersonalType,
areXeroSettingsInErrorFields,
xeroSettingsPendingAction,
};

export type {MemberEmailsToAccountIDs};
export type {MemberEmailsToAccountIDs, XeroSettings};
56 changes: 38 additions & 18 deletions 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,36 @@ 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 updatePolicyConnectionConfig<TConnectionName extends ConnectionNameExceptNetSuite, TSettingName extends keyof Connections[TConnectionName]['config']>(
policyID: string,
connectionName: TConnectionName,
Expand All @@ -51,12 +83,8 @@ function updatePolicyConnectionConfig<TConnectionName extends ConnectionNameExce
[connectionName]: {
config: {
[settingName]: settingValue ?? null,
pendingFields: {
[settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
errorFields: {
[settingName]: null,
},
pendingFields: createPendingFields(settingName, settingValue, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE),
errorFields: createErrorFields(settingName, settingValue, null),
},
},
},
Expand All @@ -73,12 +101,8 @@ function updatePolicyConnectionConfig<TConnectionName extends ConnectionNameExce
[connectionName]: {
config: {
[settingName]: settingValue ?? null,
pendingFields: {
[settingName]: null,
},
errorFields: {
[settingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
pendingFields: createPendingFields(settingName, settingValue, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE),
errorFields: createErrorFields(settingName, settingValue, ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')),
},
},
},
Expand All @@ -95,12 +119,8 @@ function updatePolicyConnectionConfig<TConnectionName extends ConnectionNameExce
[connectionName]: {
config: {
[settingName]: settingValue ?? null,
pendingFields: {
[settingName]: null,
},
errorFields: {
[settingName]: null,
},
pendingFields: createPendingFields(settingName, settingValue, null),
errorFields: createErrorFields(settingName, settingValue, null),
},
},
},
Expand Down
54 changes: 48 additions & 6 deletions src/pages/workspace/accounting/PolicyAccountingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,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 @@ -62,8 +72,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 @@ -72,6 +85,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 @@ -102,9 +116,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 @@ -168,7 +196,7 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) {
!((name === CONST.POLICY.CONNECTIONS.NAME.NETSUITE && !canUseNetSuiteIntegration) || (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 @@ -220,8 +248,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 @@ -286,7 +316,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 @@ -335,6 +365,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 @@ -343,6 +377,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 @@ -360,6 +398,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
Loading