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

Feat/xero import customer #41377

Merged
merged 10 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/xero/organization/:currentOrganizationID',
getRoute: (policyID: string, currentOrganizationID: string) => `settings/workspaces/${policyID}/accounting/xero/organization/${currentOrganizationID}` as const,
},
POLICY_ACCOUNTING_XERO_CUSTOMER: {
route: '/settings/workspaces/:policyID/accounting/xero/import/customers',
getRoute: (policyID: string) => `/settings/workspaces/${policyID}/accounting/xero/import/customers` as const,
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ const SCREENS = {
QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Quickbooks_Online_Invoice_Account_Selector',
XERO_IMPORT: 'Policy_Accounting_Xero_Import',
XERO_ORGANIZATION: 'Policy_Accounting_Xero_Customers',
XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer',
},
INITIAL: 'Workspace_Initial',
PROFILE: 'Workspace_Profile',
Expand Down
64 changes: 64 additions & 0 deletions src/components/ConnectionLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {PolicyAccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {TranslationPaths} from '@src/languages/types';
import type {PolicyFeatureName} from '@src/types/onyx/Policy';
import HeaderWithBackButton from './HeaderWithBackButton';
import ScreenWrapper from './ScreenWrapper';
import ScrollView from './ScrollView';
import Text from './Text';

type ConnectionLayoutProps = {
/** Used to set the testID for tests */
displayName: string;
/** Header title for the connection */
headerTitle: TranslationPaths;
/** React nodes that will be shown */
children?: React.ReactNode;
/** Title of the connection component */
title?: TranslationPaths;
/** Subtitle of the connection */
subtitle?: TranslationPaths;
/** The current policyID */
policyID: string;
/** Defines which types of access should be verified */
accessVariants?: PolicyAccessVariant[];
/** The current feature name that the user tries to get access to */
featureName?: PolicyFeatureName;
};

function ConnectionLayout({displayName, headerTitle, children, title, subtitle, policyID, accessVariants, featureName}: ConnectionLayoutProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

return (
<AccessOrNotFoundWrapper
policyID={policyID}
accessVariants={accessVariants}
featureName={featureName}
>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
shouldEnableMaxHeight
testID={displayName}
>
<HeaderWithBackButton
title={translate(headerTitle)}
onBackButtonPress={() => Navigation.goBack()}
/>
<ScrollView contentContainerStyle={[styles.pb2, styles.ph5]}>
<View style={[styles.pb2]}>{title && <Text style={styles.pb5}>{translate(title)}</Text>}</View>
{subtitle && <Text style={styles.textLabelSupporting}>{translate(subtitle)}</Text>}
{children}
</ScrollView>
</ScreenWrapper>
</AccessOrNotFoundWrapper>
);
}

ConnectionLayout.displayName = 'ConnectionLayout';
export default ConnectionLayout;
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1994,6 +1994,7 @@ export default {
importDescription: 'Choose which coding configurations are imported from Xero to Expensify.',
trackingCategories: 'Tracking categories',
customers: 'Re-bill customers',
customersDescription: 'Import customer contacts. Billable expenses need tags for export. Expenses will carry the customer information to Xero for sales invoices.',
},
type: {
free: 'Free',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2021,6 +2021,8 @@ export default {
importDescription: 'Elija qué configuraciones de codificación se importan de Xero a Expensify.',
trackingCategories: 'Categorías de seguimiento',
customers: 'Volver a facturar a los clientes',
customersDescription:
'Importar contactos de clientes. Los gastos facturables necesitan etiquetas para la exportación. Los gastos llevarán la información del cliente a Xero para las facturas de ventas.',
},
type: {
free: 'Gratis',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP

[SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: () => require('../../../../pages/workspace/accounting/xero/XeroImportPage').default as React.ComponentType,
[SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: () => require('../../../../pages/workspace/accounting/xero/XeroOrganizationConfigurationPage').default as React.ComponentType,
[SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: () => require('../../../../pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () =>
require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial<Record<FullScreenName, string[]>> = {
SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_INVOICE_ACCOUNT_SELECTOR,
SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT,
SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION,
SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER,
],
[SCREENS.WORKSPACE.TAXES]: [
SCREENS.WORKSPACE.TAXES_SETTINGS,
Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ORGANIZATION.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: {path: ROUTES.POLICY_ACCOUNTING_XERO_CUSTOMER.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
},
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.XERO_IMPORT]: {
policyID: string;
};
[SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: {
policyID: string;
};
[SCREENS.WORKSPACE.ACCOUNTING.XERO_ORGANIZATION]: {
policyID: string;
organizationID: string;
Expand Down
2 changes: 2 additions & 0 deletions src/pages/workspace/AccessOrNotFoundWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps
return callOrReturn(props.children, props);
}

export type {PolicyAccessVariant};

export default withOnyx<AccessOrNotFoundWrapperProps, AccessOrNotFoundWrapperOnyxProps>({
policy: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`,
Expand Down
1 change: 1 addition & 0 deletions src/pages/workspace/accounting/PolicyAccountingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting
}
return policy?.connections?.xero?.data?.tenants ?? [];
}, [policy]);

hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
const currentXeroOrganization = tenants.find((tenant) => tenant.id === policy?.connections?.xero.config.tenantID);

const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo(
Expand Down
7 changes: 6 additions & 1 deletion src/pages/workspace/accounting/xero/XeroImportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import withPolicy from '@pages/workspace/withPolicy';
import type {WithPolicyProps} from '@pages/workspace/withPolicy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Tenant} from '@src/types/onyx/Policy';

function XeroImportPage({policy}: WithPolicyProps) {
Expand Down Expand Up @@ -41,7 +43,9 @@ function XeroImportPage({policy}: WithPolicyProps) {
},
{
description: translate('workspace.xero.customers'),
action: () => {},
action: () => {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_CUSTOMER.getRoute(policyID));
},
hasError: !!policy?.errors?.importCustomers,
title: importCustomers ? translate('workspace.accounting.importedAsTags') : '',
hungvu193 marked this conversation as resolved.
Show resolved Hide resolved
pendingAction: pendingFields?.importCustomers,
Expand All @@ -67,6 +71,7 @@ function XeroImportPage({policy}: WithPolicyProps) {
policy?.errors?.importCustomers,
policy?.errors?.importTaxes,
translate,
policyID,
],
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import {View} from 'react-native';
import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Switch from '@components/Switch';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections';
import type {WithPolicyProps} from '@pages/workspace/withPolicy';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import variables from '@styles/variables';
import CONST from '@src/CONST';

function XeroCustomerConfigurationPage({policy}: WithPolicyProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
const policyID = policy?.id ?? '';
const {syncCustomers, pendingFields} = policy?.connections?.xero?.config ?? {};

const isSwitchOn = Boolean(syncCustomers && syncCustomers !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE);
const isReportFieldsSelected = syncCustomers === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD;
return (
<ConnectionLayout
displayName={XeroCustomerConfigurationPage.displayName}
headerTitle="workspace.xero.customers"
title="workspace.xero.customersDescription"
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
>
<View>
<View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}>
<View style={styles.flex1}>
<Text fontSize={variables.fontSizeNormal}>{translate('workspace.accounting.import')}</Text>
</View>
<OfflineWithFeedback pendingAction={pendingFields?.syncCustomers}>
<View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}>
<Switch
accessibilityLabel={translate('workspace.xero.customers')}
isOn={isSwitchOn}
onToggle={() =>
Connections.updatePolicyConnectionConfig(
policyID,
CONST.POLICY.CONNECTIONS.NAME.XERO,
CONST.QUICK_BOOKS_CONFIG.SYNC_CUSTOMERS,
isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG,
)
}
/>
</View>
</OfflineWithFeedback>
</View>
{isSwitchOn && (
<OfflineWithFeedback pendingAction={pendingFields?.syncCustomers}>
<MenuItemWithTopDescription
interactive={false}
title={isReportFieldsSelected ? translate('workspace.common.reportFields') : translate('workspace.common.tags')}
description={translate('workspace.qbo.displayedAs')}
wrapperStyle={styles.sectionMenuItemTopDescription}
/>
</OfflineWithFeedback>
)}
</View>
</ConnectionLayout>
);
}

XeroCustomerConfigurationPage.displayName = 'XeroCustomerConfigurationPage';

export default withPolicyConnections(XeroCustomerConfigurationPage);
1 change: 1 addition & 0 deletions src/types/onyx/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ type XeroConnectionConfig = OnyxCommon.OnyxValueWithOfflineFeedback<{
importCustomers: boolean;
importTaxRates: boolean;
importTrackingCategories: boolean;
syncCustomers: IntegrationEntityMap;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove syncCustomers and use importCustomers.
The docs isn't accurate. The correct key is importCustomers, and we'll have to update it everywhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm getting errors from server, so I can't send the API request, but the server response syncCustomers.

Copy link
Member

@rushatgabhane rushatgabhane May 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh interesting, my bad then 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the issue now, let me fix it

Copy link
Contributor Author

@hungvu193 hungvu193 May 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I updated it to importCustomers API still throw errors so I'm asking for help.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's BE issue so I don't think it's related, bump @lakchote for helping here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the request:

curl 'https://dev.new.expensify.com:8082/api/UpdatePolicyConnectionConfiguration?' \
  -H 'Accept: */*' \
  -H 'Accept-Language: en-US,en;q=0.9' \
  -H 'Connection: keep-alive' \
  -H 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundarygHxrwBguyZCnbbdZ' \
  -H 'Cookie: initialReferer=https%3A%2F%2Fdev.new.expensify.com%3A8082%2F; browserGUID=657b1e1ccf98b; mutiny.user.token=88dfd70a-3aa5-4ad9-9080-4f54dbfb73ef; _gcl_au=1.1.1489038878.1709459015; _ga=GA1.1.188411470.1709459015; cf_clearance=M1DirP9FL5i8zejTc7rb_GLuOKdGsyp3cgg9jOJI7rU-1714555619-1.0.1.1-F.I358ic4k57LOYGQU6UmFW1pLakSPDCfFqWltRyb0CZ0yyFgbgxdsmlwThvn4glOQr1Tld4QC0AGAoFQost4Q; _clck=8v84vb%7C2%7Cfle%7C0%7C1523; _uetsid=f5a8bd70079c11efb4b61be823255bec; _uetvid=80f5ea40d94211eea783efc64819d0ae; _clsk=1jc628l%7C1714557292125%7C3%7C1%7Ct.clarity.ms%2Fcollect; _ga_6BR2QJRCCD=GS1.1.1714555619.10.1.1714557292.60.0.0; _cfuvid=onvcaqxpOn013xLmz_Y_h0WpFucfhvDpjK4TEC1k75s-1714568731274-0.0.1.1-604800000; __cf_bm=58TeQrCPXirZSdhJBZkR1rHYuF89iKk9FZrMPLwQzcE-1714572465-1.0.1.1-RcyXwMqSPocs9YpED92uIGK8EObzH5o0fev3LWMVPuxVDR_aR.B4vvdo7_5VMQwZBkZybZ3AOy4.akQlxwlG5Q' \
  -H 'Origin: https://dev.new.expensify.com:8082' \
  -H 'Referer: https://dev.new.expensify.com:8082/settings/workspaces/67D374BFA1B0F027/accounting/xero/import' \
  -H 'Sec-Fetch-Dest: empty' \
  -H 'Sec-Fetch-Mode: cors' \
  -H 'Sec-Fetch-Site: same-origin' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' \
  -H 'sec-ch-ua: "Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "macOS"' \
  --data-raw $'------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="policyID"\r\n\r\n67D374BFA1B0F027\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="connectionName"\r\n\r\nxero\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="settingName"\r\n\r\nimportCustomers\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="settingValue"\r\n\r\nfalse\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="idempotencyKey"\r\n\r\nimportCustomers\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="appversion"\r\n\r\n1.4.69-0\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="apiRequestType"\r\n\r\nwrite\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="pusherSocketID"\r\n\r\n708991.54308\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="shouldRetry"\r\n\r\ntrue\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="canCancel"\r\n\r\ntrue\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="authToken"\r\n\r\nAE80628CBA1BD23729B5986BDC63B98B807C33BF8C268A74CA95847D3ABA55BF75DE93D7C9A5ADC8D72676FDC130C1F765717BEBEBFF894EBEE5CD15BA2EBB8C7CDE486549F1E2CFE99361A57973938EEDF74E4F26996768CA32109FB28DE4D51B9DD33FEDCC5A804ECF658DC9B4718CDD1893766D3DBE718971ECC10B6973CFEBC8FDBA80FE1EC9A1A2871DD6FF5DBDAC33DA2A899E17EE74701773482BCC36CEFCA335FFB1A6976D4187FA7CCF87388B642F8DBAEDBD3C4ACA09686CFF24FDE6C8B2B96167B37A179456265E63985865176CE461AC6CBF6FF000C295AF538DF6947F5D2B401A14BCCB6D7790293919197F9DC445260B9CA84658AC0A9CD932D41DA070BA547BA4294E5184E18FFC05E7F386D025AC26D89AB449A0591A2036FA01131D6E9A05804DF210192B7B39AED0EA35B960CCB3B2BEFAD8C2B0FB769CDDF4936D7174D8354E5071EBE77E4029A06ACEC187A5FBFCD155CB6CFE990AB2\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="referer"\r\n\r\necash\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="platform"\r\n\r\nweb\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="api_setCookie"\r\n\r\nfalse\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="email"\r\n\r\[email protected]\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ\r\nContent-Disposition: form-data; name="isFromDevEnv"\r\n\r\ntrue\r\n------WebKitFormBoundarygHxrwBguyZCnbbdZ--\r\n'

Copy link
Contributor

@lakchote lakchote May 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a backend issue. The correct key (importCustomers) needs be added as a valid connection key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @lakchote , Should we hold this PR for BE changes?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR to fix that is up here (internal engineers).

isConfigured: boolean;
mappings: {
customer: string;
Expand Down
Loading