Skip to content

Commit c52ed2b

Browse files
authored
Merge pull request #55412 from software-mansion-labs/feature/kuba_nowakowski/add_switch_animations_to_acounting
Added working animation to switch in accounting page
2 parents f1b5896 + dc6671f commit c52ed2b

23 files changed

Lines changed: 475 additions & 184 deletions

src/components/Accordion/index.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type AccordionProps = {
2424

2525
function Accordion({isExpanded, children, duration = 300, isToggleTriggered, style}: AccordionProps) {
2626
const height = useSharedValue(0);
27+
const isAnimating = useSharedValue(false);
2728

2829
const derivedHeight = useDerivedValue(() => {
2930
if (!isToggleTriggered.get()) {
@@ -41,23 +42,37 @@ function Accordion({isExpanded, children, duration = 300, isToggleTriggered, sty
4142
return isExpanded.get() ? 1 : 0;
4243
}
4344

44-
return withTiming(isExpanded.get() ? 1 : 0, {
45-
duration,
46-
easing: Easing.inOut(Easing.quad),
47-
});
45+
isAnimating.set(true);
46+
return withTiming(
47+
isExpanded.get() ? 1 : 0,
48+
{
49+
duration,
50+
easing: Easing.inOut(Easing.quad),
51+
},
52+
(finished) => {
53+
if (!finished || !isExpanded.get()) {
54+
return;
55+
}
56+
isAnimating.set(false);
57+
},
58+
);
4859
});
4960

5061
const animatedStyle = useAnimatedStyle(() => {
5162
if (!isToggleTriggered.get() && !isExpanded.get()) {
5263
return {
5364
height: 0,
5465
opacity: 0,
66+
display: 'none',
5567
};
5668
}
5769

5870
return {
5971
height: !isToggleTriggered.get() ? undefined : derivedHeight.get(),
72+
maxHeight: !isToggleTriggered.get() ? undefined : derivedHeight.get(),
6073
opacity: derivedOpacity.get(),
74+
overflow: isAnimating.get() ? 'hidden' : 'visible',
75+
display: isExpanded.get() ? 'inline' : 'none',
6176
};
6277
});
6378

src/hooks/useAccordionAnimation.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {useEffect} from 'react';
2+
import {useSharedValue} from 'react-native-reanimated';
3+
4+
/**
5+
* @returns two values: isExpanded, which manages the expansion of the accordion component,
6+
* and shouldAnimateAccordionSection, which determines whether we should animate
7+
* the expanding and collapsing of the accordion based on changes in isExpanded.
8+
*/
9+
function useAccordionAnimation(isExpanded: boolean) {
10+
const isAccordionExpanded = useSharedValue(isExpanded);
11+
const shouldAnimateAccordionSection = useSharedValue(false);
12+
const hasMounted = useSharedValue(false);
13+
14+
useEffect(() => {
15+
isAccordionExpanded.set(isExpanded);
16+
if (hasMounted.get()) {
17+
shouldAnimateAccordionSection.set(true);
18+
} else {
19+
hasMounted.set(true);
20+
}
21+
}, [hasMounted, isAccordionExpanded, isExpanded, shouldAnimateAccordionSection]);
22+
23+
return {isAccordionExpanded, shouldAnimateAccordionSection};
24+
}
25+
26+
export default useAccordionAnimation;

src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React, {useMemo} from 'react';
2+
import Accordion from '@components/Accordion';
23
import ConnectionLayout from '@components/ConnectionLayout';
34
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
45
import OfflineWithFeedback from '@components/OfflineWithFeedback';
6+
import useAccordionAnimation from '@hooks/useAccordionAnimation';
57
import useLocalize from '@hooks/useLocalize';
68
import useThemeStyles from '@hooks/useThemeStyles';
7-
import * as ErrorUtils from '@libs/ErrorUtils';
9+
import {getLatestErrorField} from '@libs/ErrorUtils';
810
import {areSettingsInErrorFields, getCurrentSageIntacctEntityName, settingsPendingAction} from '@libs/PolicyUtils';
911
import Navigation from '@navigation/Navigation';
1012
import type {WithPolicyProps} from '@pages/workspace/withPolicy';
@@ -17,7 +19,7 @@ import {
1719
updateSageIntacctSyncReimbursedReports,
1820
updateSageIntacctSyncReimbursementAccountID,
1921
} from '@userActions/connections/SageIntacct';
20-
import * as Policy from '@userActions/Policy/Policy';
22+
import {clearSageIntacctErrorField} from '@userActions/Policy/Policy';
2123
import CONST from '@src/CONST';
2224
import ROUTES from '@src/ROUTES';
2325
import type {SageIntacctDataElement} from '@src/types/onyx/Policy';
@@ -34,6 +36,8 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) {
3436
const {importEmployees, autoSync, sync, pendingFields, errorFields} = policy?.connections?.intacct?.config ?? {};
3537
const {data, config} = policy?.connections?.intacct ?? {};
3638

39+
const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(!!sync?.syncReimbursedReports);
40+
3741
const toggleSections = useMemo(
3842
() => [
3943
{
@@ -42,8 +46,8 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) {
4246
isActive: !!autoSync?.enabled,
4347
onToggle: (enabled: boolean) => updateSageIntacctAutoSync(policyID, enabled),
4448
subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED],
45-
error: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED),
46-
onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED),
49+
error: getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED),
50+
onCloseError: () => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED),
4751
},
4852
{
4953
label: translate('workspace.sageIntacct.inviteEmployees'),
@@ -54,12 +58,10 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) {
5458
updateSageIntacctApprovalMode(policyID, enabled);
5559
},
5660
subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE],
57-
error:
58-
ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES) ??
59-
ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE),
61+
error: getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES) ?? getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE),
6062
onCloseError: () => {
61-
Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES);
62-
Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE);
63+
clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES);
64+
clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE);
6365
},
6466
},
6567
{
@@ -75,9 +77,9 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) {
7577
}
7678
},
7779
subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS],
78-
error: ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS),
80+
error: getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS),
7981
onCloseError: () => {
80-
Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS);
82+
clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS);
8183
},
8284
},
8385
],
@@ -113,20 +115,23 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) {
113115
/>
114116
))}
115117

116-
{!!sync?.syncReimbursedReports && (
118+
<Accordion
119+
isExpanded={isAccordionExpanded}
120+
isToggleTriggered={shouldAnimateAccordionSection}
121+
>
117122
<OfflineWithFeedback
118123
key={translate('workspace.sageIntacct.paymentAccount')}
119124
pendingAction={settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID], pendingFields)}
120125
>
121126
<MenuItemWithTopDescription
122-
title={getReimbursedAccountName(data?.bankAccounts ?? [], sync.reimbursementAccountID) ?? translate('workspace.sageIntacct.notConfigured')}
127+
title={getReimbursedAccountName(data?.bankAccounts ?? [], sync?.reimbursementAccountID) ?? translate('workspace.sageIntacct.notConfigured')}
123128
description={translate('workspace.sageIntacct.paymentAccount')}
124129
shouldShowRightIcon
125130
onPress={() => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.getRoute(policyID))}
126131
brickRoadIndicator={areSettingsInErrorFields([CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID], errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
127132
/>
128133
</OfflineWithFeedback>
129-
)}
134+
</Accordion>
130135
</ConnectionLayout>
131136
);
132137
}

src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,24 @@
11
import React from 'react';
2+
import Accordion from '@components/Accordion';
23
import ConnectionLayout from '@components/ConnectionLayout';
34
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
45
import OfflineWithFeedback from '@components/OfflineWithFeedback';
6+
import useAccordionAnimation from '@hooks/useAccordionAnimation';
57
import useLocalize from '@hooks/useLocalize';
68
import useThemeStyles from '@hooks/useThemeStyles';
7-
import * as ErrorUtils from '@libs/ErrorUtils';
9+
import {getLatestErrorField} from '@libs/ErrorUtils';
810
import Navigation from '@libs/Navigation/Navigation';
911
import {areSettingsInErrorFields, getSageIntacctNonReimbursableActiveDefaultVendor, settingsPendingAction} from '@libs/PolicyUtils';
10-
import type {MenuItem, ToggleItem} from '@pages/workspace/accounting/intacct/types';
12+
import type {ExtendedMenuItemWithSubscribedSettings, MenuItemToRender} from '@pages/workspace/accounting/intacct/types';
1113
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
1214
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
1315
import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
1416
import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct';
15-
import * as Policy from '@userActions/Policy/Policy';
17+
import {clearSageIntacctErrorField} from '@userActions/Policy/Policy';
1618
import CONST from '@src/CONST';
1719
import ROUTES from '@src/ROUTES';
1820
import {getDefaultVendorName} from './utils';
1921

20-
type MenuItemWithSubscribedSettings = Pick<MenuItem, 'type' | 'description' | 'title' | 'onPress' | 'shouldHide'> & {subscribedSettings?: string[]};
21-
22-
type ToggleItemWithKey = ToggleItem & {key: string};
23-
2422
function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsProps) {
2523
const {translate} = useLocalize();
2624
const policyID = policy?.id ?? '-1';
@@ -29,8 +27,31 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsP
2927

3028
const activeDefaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy);
3129
const defaultVendorName = getDefaultVendorName(activeDefaultVendor, intacctData?.vendors);
30+
const expandedCondition = !(
31+
!config?.export.nonReimbursable ||
32+
(config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && !config?.export.nonReimbursableCreditCardChargeDefaultVendor)
33+
);
34+
35+
const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(expandedCondition);
36+
37+
const renderDefault = (item: MenuItemToRender) => {
38+
return (
39+
<OfflineWithFeedback
40+
key={item.description}
41+
pendingAction={settingsPendingAction(item.subscribedSettings, config?.pendingFields)}
42+
>
43+
<MenuItemWithTopDescription
44+
title={item.title}
45+
description={item.description}
46+
shouldShowRightIcon
47+
onPress={item?.onPress}
48+
brickRoadIndicator={areSettingsInErrorFields(item.subscribedSettings, config?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
49+
/>
50+
</OfflineWithFeedback>
51+
);
52+
};
3253

33-
const menuItems: Array<MenuItemWithSubscribedSettings | ToggleItemWithKey> = [
54+
const menuItems: ExtendedMenuItemWithSubscribedSettings[] = [
3455
{
3556
type: 'menuitem',
3657
title: config?.export.nonReimbursable
@@ -62,27 +83,38 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsP
6283
onToggle: (enabled) => {
6384
const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : '';
6485
updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, vendor, config?.export.nonReimbursableCreditCardChargeDefaultVendor);
86+
isAccordionExpanded.set(enabled);
87+
shouldAnimateAccordionSection.set(true);
6588
},
66-
onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR),
89+
onCloseError: () => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR),
6790
pendingAction: settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR], config?.pendingFields),
68-
errors: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR),
91+
errors: getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR),
6992
shouldHide: config?.export.nonReimbursable !== CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE,
7093
},
7194
{
72-
type: 'menuitem',
73-
title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'),
74-
description: translate('workspace.sageIntacct.defaultVendor'),
75-
onPress: () => {
76-
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE.toLowerCase()));
77-
},
78-
subscribedSettings: [
79-
config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL
80-
? CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR
81-
: CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR,
95+
type: 'accordion',
96+
children: [
97+
{
98+
type: 'menuitem',
99+
title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'),
100+
description: translate('workspace.sageIntacct.defaultVendor'),
101+
onPress: () => {
102+
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE.toLowerCase()));
103+
},
104+
subscribedSettings: [
105+
config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL
106+
? CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR
107+
: CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR,
108+
],
109+
shouldHide:
110+
!config?.export.nonReimbursable ||
111+
(config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE &&
112+
!config?.export.nonReimbursableCreditCardChargeDefaultVendor),
113+
},
82114
],
83-
shouldHide:
84-
!config?.export.nonReimbursable ||
85-
(config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && !config?.export.nonReimbursableCreditCardChargeDefaultVendor),
115+
shouldHide: false,
116+
shouldExpand: isAccordionExpanded,
117+
shouldAnimateSection: shouldAnimateAccordionSection,
86118
},
87119
];
88120

@@ -114,21 +146,17 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsP
114146
wrapperStyle={[styles.mv3, styles.ph5]}
115147
/>
116148
);
117-
default:
149+
case 'accordion':
118150
return (
119-
<OfflineWithFeedback
120-
key={item.description}
121-
pendingAction={settingsPendingAction(item.subscribedSettings, config?.pendingFields)}
151+
<Accordion
152+
isExpanded={item.shouldExpand}
153+
isToggleTriggered={item.shouldAnimateSection}
122154
>
123-
<MenuItemWithTopDescription
124-
title={item.title}
125-
description={item.description}
126-
shouldShowRightIcon
127-
onPress={item?.onPress}
128-
brickRoadIndicator={areSettingsInErrorFields(item.subscribedSettings, config?.errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
129-
/>
130-
</OfflineWithFeedback>
155+
{item.children.map((child) => renderDefault(child))}
156+
</Accordion>
131157
);
158+
default:
159+
return renderDefault(item);
132160
}
133161
})}
134162
</ConnectionLayout>

0 commit comments

Comments
 (0)