From 81870fabf83dd4855114c94f88e7655494455481 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 31 Jul 2025 00:43:05 +0800 Subject: [PATCH 0001/1015] update type and adjust code --- src/components/ScreenWrapper/index.tsx | 4 ++-- src/pages/home/ReportScreen.tsx | 30 +++++++++++++++++--------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/components/ScreenWrapper/index.tsx b/src/components/ScreenWrapper/index.tsx index a2d6474b21c0..e8e398d8b4a9 100644 --- a/src/components/ScreenWrapper/index.tsx +++ b/src/components/ScreenWrapper/index.tsx @@ -19,7 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import NarrowPaneContext from '@libs/Navigation/AppNavigator/Navigators/NarrowPaneContext'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportsSplitNavigatorParamList, RootNavigatorParamList} from '@libs/Navigation/types'; +import type {ReportsSplitNavigatorParamList, RootNavigatorParamList, SearchReportParamList} from '@libs/Navigation/types'; import closeReactNativeApp from '@userActions/HybridApp'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; @@ -47,7 +47,7 @@ type ScreenWrapperProps = Omit & * * This is required because transitionEnd event doesn't trigger in the testing environment. */ - navigation?: PlatformStackNavigationProp | PlatformStackNavigationProp; + navigation?: PlatformStackNavigationProp | PlatformStackNavigationProp | PlatformStackNavigationProp; /** A unique ID to find the screen wrapper in tests */ testID: string; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index a368aece25cf..88d966690b26 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -76,7 +76,7 @@ import { isValidReportIDFromPath, } from '@libs/ReportUtils'; import {isNumeric} from '@libs/ValidationUtils'; -import type {ReportsSplitNavigatorParamList} from '@navigation/types'; +import type {ReportsSplitNavigatorParamList, SearchReportParamList} from '@navigation/types'; import {setShouldShowComposeInput} from '@userActions/Composer'; import { clearDeleteTransactionNavigateBackUrl, @@ -101,7 +101,9 @@ import ReportFooter from './report/ReportFooter'; import type {ActionListContextType, ScrollPosition} from './ReportScreenContext'; import {ActionListContext} from './ReportScreenContext'; -type ReportScreenNavigationProps = PlatformStackScreenProps; +type ReportScreenNavigationProps = + | PlatformStackScreenProps + | PlatformStackScreenProps; type ReportScreenProps = ReportScreenNavigationProps; @@ -141,6 +143,7 @@ function getParentReportAction(parentReportActions: OnyxEntry Date: Thu, 31 Jul 2025 00:43:26 +0800 Subject: [PATCH 0002/1015] navigate to the last report route --- .../Navigation/NavigationTabBar/index.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx index ea34b3f0f122..cca0b2df396b 100644 --- a/src/components/Navigation/NavigationTabBar/index.tsx +++ b/src/components/Navigation/NavigationTabBar/index.tsx @@ -32,7 +32,7 @@ import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; import Navigation from '@navigation/Navigation'; import navigationRef from '@navigation/navigationRef'; -import type {RootNavigatorParamList, SearchFullscreenNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types'; +import type {ReportsSplitNavigatorParamList, RootNavigatorParamList, SearchFullscreenNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types'; import NavigationTabBarAvatar from '@pages/home/sidebar/NavigationTabBarAvatar'; import NavigationTabBarFloatingActionButton from '@pages/home/sidebar/NavigationTabBarFloatingActionButton'; import variables from '@styles/variables'; @@ -116,8 +116,24 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar } hideInboxTooltip(); + if (shouldUseNarrowLayout) { + Navigation.navigate(ROUTES.HOME); + return; + } + + const rootState = navigationRef.getRootState() as State; + const lastReportNavigator = rootState.routes.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); + const lastReportNavigatorState = lastReportNavigator && lastReportNavigator.key ? getPreservedNavigatorState(lastReportNavigator?.key) : undefined; + const lastReportRoute = lastReportNavigatorState?.routes.findLast((route) => route.name === SCREENS.REPORT); + + if (lastReportRoute) { + const {reportID} = lastReportRoute.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]; + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + return; + } + Navigation.navigate(ROUTES.HOME); - }, [hideInboxTooltip, selectedTab]); + }, [hideInboxTooltip, selectedTab, shouldUseNarrowLayout]); const navigateToSearch = useCallback(() => { if (selectedTab === NAVIGATION_TABS.SEARCH) { From 1a9aa39705d49f0c7eda11890812c43d58d4cb12 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 31 Jul 2025 01:07:41 +0800 Subject: [PATCH 0003/1015] lint --- src/pages/home/ReportScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 88d966690b26..0c335c6432a5 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -200,7 +200,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { Log.info(`[ReportScreen] no reportID found in params, setting it to lastAccessedReportID: ${lastAccessedReportID}`); navigation.setParams({reportID: lastAccessedReportID}); - }, [isBetaEnabled, navigation, route]); + }, [isBetaEnabled, navigation, route, isReportInRHP]); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); const chatWithAccountManagerText = useMemo(() => { From 710b10ca5ca825245c4bdec14b780acaf84b1d65 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sun, 3 Aug 2025 23:25:23 +0800 Subject: [PATCH 0004/1015] prettier --- src/pages/home/ReportScreen.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 10fbe78a3563..a46b61467f37 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -495,16 +495,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } openReport(reportIDFromRoute, reportActionIDFromRoute); - }, [ - reportMetadata.isOptimisticReport, - report, - isOffline, - route.params, - isReportInRHP, - currentUserEmail, - reportIDFromRoute, - reportActionIDFromRoute, - ]); + }, [reportMetadata.isOptimisticReport, report, isOffline, route.params, isReportInRHP, currentUserEmail, reportIDFromRoute, reportActionIDFromRoute]); const prevTransactionThreadReportID = usePrevious(transactionThreadReportID); useEffect(() => { From 940962de6f69846955e042655a5e9ba7de6e76e7 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 5 Aug 2025 11:49:57 +0800 Subject: [PATCH 0005/1015] add comment --- src/components/Navigation/NavigationTabBar/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx index cca0b2df396b..08e9cc51d175 100644 --- a/src/components/Navigation/NavigationTabBar/index.tsx +++ b/src/components/Navigation/NavigationTabBar/index.tsx @@ -121,6 +121,7 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar return; } + // On large screens, we want to reopen the last report available in the navigation stack const rootState = navigationRef.getRootState() as State; const lastReportNavigator = rootState.routes.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); const lastReportNavigatorState = lastReportNavigator && lastReportNavigator.key ? getPreservedNavigatorState(lastReportNavigator?.key) : undefined; From f29964a0c39538708538e61f4d8335ef654bc0c8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 12 Sep 2025 13:03:50 +0800 Subject: [PATCH 0006/1015] navigate to the last report route --- .../Navigation/NavigationTabBar/index.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx index 354b8fee66ff..509bfead2f4d 100644 --- a/src/components/Navigation/NavigationTabBar/index.tsx +++ b/src/components/Navigation/NavigationTabBar/index.tsx @@ -131,6 +131,22 @@ function NavigationTabBar({selectedTab, isTooltipAllowed = false, isTopLevelBar } hideInboxTooltip(); + if (shouldUseNarrowLayout) { + Navigation.navigate(ROUTES.HOME); + return; + } + + const rootState = navigationRef.getRootState() as State; + const lastReportNavigator = rootState.routes.findLast((route) => route.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); + const lastReportNavigatorState = lastReportNavigator && lastReportNavigator.key ? getPreservedNavigatorState(lastReportNavigator?.key) : undefined; + const lastReportRoute = lastReportNavigatorState?.routes.findLast((route) => route.name === SCREENS.REPORT); + + if (lastReportRoute) { + const {reportID} = lastReportRoute.params as ReportsSplitNavigatorParamList[typeof SCREENS.REPORT]; + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + return; + } + Navigation.navigate(ROUTES.HOME); }, [hideInboxTooltip, selectedTab]); From fe298060f2205e1765d351a4b792703eb06110ec Mon Sep 17 00:00:00 2001 From: dmkt9 Date: Tue, 11 Nov 2025 11:11:51 +0700 Subject: [PATCH 0007/1015] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 36 ++++++- tests/navigation/NavigateTests.tsx | 101 ++++++++++++++++++++ tests/utils/TestNavigationContainer.tsx | 24 +++++ 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 1649e9c7ab29..535754f57409 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -64,6 +64,36 @@ function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRou return currentParams?.reportID === newParams?.reportID; } +function areFullScreenRoutesEqual(matchingFullScreenRoute: NavigationPartialRoute, lastFullScreenRoute: NavigationPartialRoute) { + const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); + const lastRouteInLastFullScreenRoute = lastFullScreenRoute.state?.routes?.at(-1); + + // We need to perform a manual check here, since it's possible to open the `WorkspaceRestrictedActionPage` via the FAB while still on the Settings page. + if (lastRouteInMatchingFullScreen?.name === SCREENS.SETTINGS.SUBSCRIPTION.ROOT && lastRouteInLastFullScreenRoute?.name !== SCREENS.SETTINGS.SUBSCRIPTION.ROOT) { + return false; + } + + const isEqualFullScreenRoute = matchingFullScreenRoute.name === lastFullScreenRoute.name; + + return isEqualFullScreenRoute; +} + +function isRoutePreloaded(currentState: PlatformStackNavigationState, matchingFullScreenRoute: NavigationPartialRoute) { + const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); + + const preloadedRoutes = currentState.preloadedRoutes; + + return preloadedRoutes.some((preloadedRoute) => { + const isMatchingFullScreenRoute = preloadedRoute.name === matchingFullScreenRoute.name; + + // Compare the last route of the preloadedRoute and the last route of the matchingFullScreenRoute to ensure the preloaded route is accepted when matching subroutes as well + const isMatchingLastRoute = + !lastRouteInMatchingFullScreen?.name || (preloadedRoute.params && 'screen' in preloadedRoute.params && preloadedRoute.params.screen === lastRouteInMatchingFullScreen?.name); + + return isMatchingFullScreenRoute && isMatchingLastRoute; + }); +} + export default function linkTo(navigation: NavigationContainerRef | null, path: Route, options?: LinkToOptions) { if (!navigation) { throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); @@ -126,9 +156,9 @@ export default function linkTo(navigation: NavigationContainerRef isFullScreenName(route.name)); - if (matchingFullScreenRoute && lastFullScreenRoute && matchingFullScreenRoute.name !== lastFullScreenRoute.name) { - const isMatchingRoutePreloaded = currentState.preloadedRoutes.some((preloadedRoute) => preloadedRoute.name === matchingFullScreenRoute.name); - if (isMatchingRoutePreloaded) { + + if (matchingFullScreenRoute && lastFullScreenRoute && !areFullScreenRoutesEqual(matchingFullScreenRoute, lastFullScreenRoute as NavigationPartialRoute)) { + if (isRoutePreloaded(currentState, matchingFullScreenRoute)) { navigation.dispatch(StackActions.push(matchingFullScreenRoute.name)); } else { const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); diff --git a/tests/navigation/NavigateTests.tsx b/tests/navigation/NavigateTests.tsx index 084c139cc84b..28825c199ff4 100644 --- a/tests/navigation/NavigateTests.tsx +++ b/tests/navigation/NavigateTests.tsx @@ -145,5 +145,106 @@ describe('Navigate', () => { expect(rootStateAfterNavigate?.index).toBe(1); expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); }); + + it('to the sub-route from a different split navigator', () => { + // Given the initialized navigation on the narrow layout with the reports split navigator + render( + , + ); + + const rootStateBeforeNavigate = navigationRef.current?.getRootState(); + const lastSplitBeforeNavigate = rootStateBeforeNavigate?.routes.at(-1); + expect(rootStateBeforeNavigate?.index).toBe(0); + expect(lastSplitBeforeNavigate?.name).toBe(NAVIGATORS.REPORTS_SPLIT_NAVIGATOR); + expect(lastSplitBeforeNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.REPORT); + + // When navigate to the page from the different split navigator + act(() => { + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); + }); + + // Then push a new split navigator to the navigation state + const rootStateAfterNavigate = navigationRef.current?.getRootState(); + expect(rootStateAfterNavigate?.index).toBe(2); + + const middleSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-2); + expect(middleSplitAfterNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR); + expect(middleSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.SUBSCRIPTION.ROOT); + + const lastSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-1); + expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.RIGHT_MODAL_NAVIGATOR); + expect(lastSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.RIGHT_MODAL.SETTINGS); + }); + + it('to the sub-route from a same split navigator', () => { + // Given the initialized navigation on the narrow layout with the settings split navigator + render( + , + ); + + const rootStateBeforeNavigate = navigationRef.current?.getRootState(); + const lastSplitBeforeNavigate = rootStateBeforeNavigate?.routes.at(-1); + expect(rootStateBeforeNavigate?.index).toBe(0); + expect(lastSplitBeforeNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR); + expect(lastSplitBeforeNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.PROFILE.ROOT); + + // When navigate to the page from the same split navigator + act(() => { + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_ADD_PAYMENT_CARD); + }); + + // Then push a new split navigator to the navigation state + const rootStateAfterNavigate = navigationRef.current?.getRootState(); + expect(rootStateAfterNavigate?.index).toBe(2); + + const middleSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-2); + expect(middleSplitAfterNavigate?.name).toBe(NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR); + expect(middleSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.SETTINGS.SUBSCRIPTION.ROOT); + + const lastSplitAfterNavigate = rootStateAfterNavigate?.routes.at(-1); + expect(lastSplitAfterNavigate?.name).toBe(NAVIGATORS.RIGHT_MODAL_NAVIGATOR); + expect(lastSplitAfterNavigate?.state?.routes.at(-1)?.name).toBe(SCREENS.RIGHT_MODAL.SETTINGS); + }); }); }); diff --git a/tests/utils/TestNavigationContainer.tsx b/tests/utils/TestNavigationContainer.tsx index 0c3370f12de0..8ae0725f83bf 100644 --- a/tests/utils/TestNavigationContainer.tsx +++ b/tests/utils/TestNavigationContainer.tsx @@ -7,6 +7,7 @@ import navigationRef from '@libs/Navigation/navigationRef'; import type { AuthScreensParamList, ReportsSplitNavigatorParamList, + RightModalNavigatorParamList, SearchFullscreenNavigatorParamList, SettingsSplitNavigatorParamList, WorkspaceSplitNavigatorParamList, @@ -21,6 +22,7 @@ const ReportsSplit = createSplitNavigator(); const SettingsSplit = createSplitNavigator(); const SearchStack = createPlatformStackNavigator(); const WorkspaceSplit = createSplitNavigator(); +const RightModalNavigatorStack = createSplitNavigator(); const getEmptyComponent = () => jest.fn(); @@ -103,6 +105,10 @@ function TestSettingsSplitNavigator() { name={SCREENS.SETTINGS.ABOUT} getComponent={getEmptyComponent} /> + ); } @@ -122,6 +128,20 @@ function TestSearchFullscreenNavigator() { ); } +function TestRightModalNavigator() { + return ( + + + + ); +} + function TestNavigationContainer({initialState}: TestNavigationContainerProps) { return ( + ); From 670b650215fee4d00db5066b164826355c10dc50 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 13 Nov 2025 15:32:47 +0200 Subject: [PATCH 0008/1015] New screen for bank account initial process --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + src/languages/en.ts | 1 + .../ModalStackNavigators/index.tsx | 2 + src/libs/Navigation/linkingConfig/config.ts | 4 + src/libs/Navigation/types.ts | 1 + .../substeps/AccountFlowEntryPoint.tsx | 255 ++++++++++++++++++ .../substeps/CountrySelection.tsx | 2 +- 8 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4912d62d141e..15b75cbea5d0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -314,6 +314,7 @@ const ROUTES = { }, SETTINGS_ADD_BANK_ACCOUNT_VERIFY_ACCOUNT: `settings/wallet/add-bank-account/${VERIFY_ACCOUNT}`, SETTINGS_ADD_US_BANK_ACCOUNT: 'settings/wallet/add-us-bank-account', + SETTINGS_ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'settings/wallet/add-us-bank-account/entry-point', SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: `settings/wallet/add-bank-account/select-country/${VERIFY_ACCOUNT}`, SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 56bd7b4bf37a..713370471718 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -105,6 +105,7 @@ const SCREENS = { ADD_BANK_ACCOUNT_VERIFY_ACCOUNT: 'Settings_Add_Bank_Account_Verify_Account', ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', ADD_US_BANK_ACCOUNT: 'Settings_Add_US_Bank_Account', + ADD_US_BANK_ACCOUNT_ENTRY_POINT: 'Settings_Add_US_Bank_Account_Entry_Point', ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: 'Settings_Add_Bank_Account_Select_Country_Verify_Account', CLOSE: 'Settings_Close', REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged', diff --git a/src/languages/en.ts b/src/languages/en.ts index 42e84614e459..54906d7035fb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2999,6 +2999,7 @@ const translations = { currencyHeader: "What's your bank account's currency?", confirmationStepHeader: 'Check your info.', confirmationStepSubHeader: 'Double check the details below, and check the terms box to confirm.', + toGetStarted: 'Add a personal bank account to receive reimbursements, pay invoices or enable the Expensify Wallet.', }, addPersonalBankAccountPage: { enterPassword: 'Enter Expensify password', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8f829ffd913d..6f3609752d65 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -391,6 +391,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/NewBankAccountVerifyAccountPage').default, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../../pages/settings/Wallet/InternationalDepositAccount').default, [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: () => require('../../../../pages/AddPersonalBankAccountPage').default, + [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT_ENTRY_POINT]: () => + require('../../../../pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint').default, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Wallet/InternationalDepositAccount/CountrySelectionVerifyAccountPage').default, [SCREENS.SETTINGS.PROFILE.STATUS]: () => require('../../../../pages/settings/Profile/CustomStatus/StatusPage').default, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 5b6bd61ada8c..96b376bbe649 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -291,6 +291,10 @@ const config: LinkingOptions['config'] = { path: ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT, exact: true, }, + [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT_ENTRY_POINT]: { + path: ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT_ENTRY_POINT, + exact: true, + }, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: { path: ROUTES.SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e67b8f8b1da7..00161502d2a2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -203,6 +203,7 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: undefined; [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_VERIFY_ACCOUNT]: undefined; [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT]: undefined; + [SCREENS.SETTINGS.ADD_US_BANK_ACCOUNT_ENTRY_POINT]: undefined; [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER]: undefined; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx new file mode 100644 index 000000000000..f99dbbb864b5 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx @@ -0,0 +1,255 @@ +import {isUserValidatedSelector} from '@selectors/Account'; +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import {Bank, Connect, Lightbulb, Lock, RotateLeft} from '@components/Icon/Expensicons'; +import LottieAnimations from '@components/LottieAnimations'; +import MenuItem from '@components/MenuItem'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getLatestError, getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import Navigation from '@navigation/Navigation'; +import WorkspaceResetBankAccountModal from '@pages/workspace/WorkspaceResetBankAccountModal'; +import {goToWithdrawalAccountSetupStep} from '@userActions/BankAccounts'; +import {openExternalLink} from '@userActions/Link'; +import {requestResetBankAccount, resetReimbursementAccount, setBankAccountSubStep, setReimbursementAccountOptionPressed} from '@userActions/ReimbursementAccount'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type AccountFlowEntryPointProps = { + /** Bank account currently in setup */ + reimbursementAccount: OnyxEntry; + + /** Callback to continue to the next step of the setup */ + onContinuePress: () => void; + + /** The workspace name */ + policyName?: string; + + /** The workspace ID */ + policyID?: string; + + /** Goes to the previous step */ + onBackButtonPress: () => void; + + /** Should show the continue setup button */ + shouldShowContinueSetupButton: boolean | null; + + /** Whether the workspace currency is set to non USD currency */ + isNonUSDWorkspace: boolean; + + /** Should ValidateCodeActionModal be displayed or not */ + isValidateCodeActionModalVisible?: boolean; + + /** Toggle ValidateCodeActionModal */ + toggleValidateCodeActionModal?: (isVisible: boolean) => void; + + /** Set step for non USD flow */ + setNonUSDBankAccountStep: (shouldShowContinueSetupButton: string | null) => void; + + /** Set step for USD flow */ + setUSDBankAccountStep: (shouldShowContinueSetupButton: string | null) => void; + + /** Method to set the state of shouldShowContinueSetupButton */ + setShouldShowContinueSetupButton?: (shouldShowContinueSetupButton: boolean) => void; +}; + +const bankInfoStepKeys = INPUT_IDS.BANK_INFO_STEP; + +function AccountFlowEntryPoint({ + policyName = '', + onBackButtonPress, + reimbursementAccount, + onContinuePress, + shouldShowContinueSetupButton, + isNonUSDWorkspace, + isValidateCodeActionModalVisible, + toggleValidateCodeActionModal, + setNonUSDBankAccountStep, + setUSDBankAccountStep, + setShouldShowContinueSetupButton, +}: AccountFlowEntryPointProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: false}); + + const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED, {canBeMissing: true}); + const errors = reimbursementAccount?.errors ?? {}; + const pendingAction = reimbursementAccount?.pendingAction ?? null; + const isAccountValidated = account?.validated ?? false; + + /** + * Prepares and redirects user to next step in the USD flow + */ + const prepareNextStep = useCallback( + (setupType: ValueOf) => { + setBankAccountSubStep(setupType); + setUSDBankAccountStep(CONST.BANK_ACCOUNT.STEP.COUNTRY); + goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.COUNTRY); + }, + [setUSDBankAccountStep], + ); + + const handleConnectManually = () => { + if (!isAccountValidated) { + setReimbursementAccountOptionPressed(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL); + toggleValidateCodeActionModal?.(true); + return; + } + + if (isNonUSDWorkspace) { + setNonUSDBankAccountStep(CONST.NON_USD_BANK_ACCOUNT.STEP.COUNTRY); + return; + } + + prepareNextStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL); + }; + + const handleConnectPlaid = () => { + if (isUserValidated) { + Navigation.navigate(ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT); + } else { + Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT); + } + }; + + return ( + + + + +
+ + + + {translate('workspace.bankAccount.connectBankAccountNote')} + + + + {shouldShowContinueSetupButton === true ? ( + + + + + ) : ( + <> + + + + )} + +
+ + {translate('common.privacy')} + openExternalLink(CONST.ENCRYPTION_AND_SECURITY_HELP_URL)} + style={[styles.flexRow, styles.alignItemsCenter]} + accessibilityLabel={translate('bankAccount.yourDataIsSecure')} + > + {translate('bankAccount.yourDataIsSecure')} + + + + + +
+ + {!!reimbursementAccount?.shouldShowResetModal && ( + + )} +
+ ); +} + +AccountFlowEntryPoint.displayName = 'AccountFlowEntryPoint'; + +export default AccountFlowEntryPoint; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/substeps/CountrySelection.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/substeps/CountrySelection.tsx index 3189517778e9..400db7ca5dae 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/substeps/CountrySelection.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/substeps/CountrySelection.tsx @@ -33,7 +33,7 @@ function CountrySelection({isEditing, onNext, formValues, resetScreenIndex, fiel const onCountrySelected = useCallback(() => { if (currentCountry === CONST.COUNTRY.US) { if (isUserValidated) { - Navigation.navigate(ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT); + Navigation.navigate(ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT_ENTRY_POINT); } else { Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT); } From 42b6dd84229daaad71afb7b06fdb3c51bccd3961 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 14 Nov 2025 18:19:19 +0200 Subject: [PATCH 0009/1015] New flow for personal bank account with plaid/manual --- src/CONST/index.ts | 5 + ...usePersonalBankAccountDetailsFormSubmit.ts | 27 +++++ src/pages/AddPersonalBankAccountPage.tsx | 27 ++--- .../PersonalInfo/PersonalInfo.tsx | 108 ++++++++++++++++++ .../PersonalInfo/substeps/AddressStep.tsx | 59 ++++++++++ .../substeps/ConfirmationStep.tsx | 83 ++++++++++++++ .../PersonalInfo/substeps/LegalNameStep.tsx | 46 ++++++++ .../PersonalInfo/substeps/PhoneNumberStep.tsx | 71 ++++++++++++ .../utils/getInitialSubstepForPersonalInfo.ts | 25 ++++ src/types/form/PersonalBankAccountForm.ts | 15 ++- 10 files changed, 445 insertions(+), 21 deletions(-) create mode 100644 src/hooks/usePersonalBankAccountDetailsFormSubmit.ts create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getInitialSubstepForPersonalInfo.ts diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 5a0fe86933af..7b70ae8bd195 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -2716,6 +2716,11 @@ const CONST = { PAY_SOMEONE: 'start/pay/manual', SPLIT_EXPENSE: 'start/split/manual', }, + PERSONAL_BANK_SUBSTEP_INDEXES: { + LEGAL_NAME: 0, + ADDRESS: 1, + PHONE_NUMBER: 2, + }, }, PLAID: { diff --git a/src/hooks/usePersonalBankAccountDetailsFormSubmit.ts b/src/hooks/usePersonalBankAccountDetailsFormSubmit.ts new file mode 100644 index 000000000000..f65276112a8f --- /dev/null +++ b/src/hooks/usePersonalBankAccountDetailsFormSubmit.ts @@ -0,0 +1,27 @@ +import type {FormOnyxKeys} from '@components/Form/types'; +import type {OnyxFormKey} from '@src/ONYXKEYS'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useStepFormSubmit from './useStepFormSubmit'; +import type {SubStepProps} from './useSubStep/types'; + +type UsePersonalBankAccountDetailsFormSubmit = Pick & { + formId?: OnyxFormKey; + fieldIds: Array>; + shouldSaveDraft: boolean; +}; + +/** + * Hook for handling submit method in Personal Bank account Details substeps. + * When user is in editing mode, we should save values only when user confirms the change + * @param onNext - callback + * @param fieldIds - field IDs for particular step + * @param shouldSaveDraft - if we should save draft values + */ +export default function usePersonalBankAccountDetailsFormSubmit({onNext, fieldIds, shouldSaveDraft}: UsePersonalBankAccountDetailsFormSubmit) { + return useStepFormSubmit({ + formId: ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM, + onNext, + fieldIds, + shouldSaveDraft, + }); +} diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index 7a5eba02fede..0bccf951a99d 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -14,12 +14,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import {addPersonalBankAccount, clearPersonalBankAccount, validatePlaidSelection} from '@userActions/BankAccounts'; +import {clearPersonalBankAccount, validatePlaidSelection} from '@userActions/BankAccounts'; import {continueSetup} from '@userActions/PaymentMethods'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import PersonalInfoPage from './settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo'; function AddPersonalBankAccountPage() { const styles = useThemeStyles(); @@ -45,23 +46,9 @@ function AddPersonalBankAccountPage() { } }, [topmostFullScreenRoute?.name]); - const submitBankAccountForm = useCallback(() => { - const bankAccounts = plaidData?.bankAccounts ?? []; - const policyID = personalBankAccount?.policyID; - const source = personalBankAccount?.source; - - const selectedPlaidBankAccount = bankAccounts.find((bankAccount) => bankAccount.plaidAccountID === selectedPlaidAccountId); - - if (selectedPlaidBankAccount) { - const bankAccountWithToken = selectedPlaidBankAccount.plaidAccessToken - ? selectedPlaidBankAccount - : { - ...selectedPlaidBankAccount, - plaidAccessToken: plaidData?.plaidAccessToken ?? '', - }; - addPersonalBankAccount(bankAccountWithToken, policyID, source); - } - }, [plaidData, selectedPlaidAccountId, personalBankAccount]); + const moveToPersonalStep = useCallback(() => { + // Add navigation to Personal info screens + }, []); const exitFlow = useCallback( (shouldContinue = false) => { @@ -110,7 +97,7 @@ function AddPersonalBankAccountPage() { isSubmitButtonVisible={(plaidData?.bankAccounts ?? []).length > 0} submitButtonText={translate('common.saveAndContinue')} scrollContextEnabled - onSubmit={submitBankAccountForm} + onSubmit={moveToPersonalStep} validate={validatePlaidSelection} style={[styles.mh5, styles.flex1]} shouldHideFixErrorsAlert diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx new file mode 100644 index 000000000000..a4d94a646299 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx @@ -0,0 +1,108 @@ +import React, {useCallback, useMemo} from 'react'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useSubStep from '@hooks/useSubStep'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; +import Navigation from '@navigation/Navigation'; +import {addPersonalBankAccount} from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import Address from './substeps/AddressStep'; +import Confirmation from './substeps/ConfirmationStep'; +import LegalName from './substeps/LegalNameStep'; +import PhoneNumber from './substeps/PhoneNumberStep'; +import getInitialSubstepForPersonalInfo from './utils/getInitialSubstepForPersonalInfo'; + +const bodyContent: Array> = [LegalName, Address, PhoneNumber, Confirmation]; + +function PersonalInfoPage() { + const {translate} = useLocalize(); + + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const [personalBankAccount] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); + + const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA, {canBeMissing: true}); + + const personalDetails = useMemo(() => { + const currentAddress = getCurrentAddress(privatePersonalDetails); + const phone = personalBankAccount?.phoneNumber ?? privatePersonalDetails?.phoneNumber; + return { + phoneNumber: (phone && parsePhoneNumber(phone, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', + legalFirstName: personalBankAccount?.legalFirstName ?? privatePersonalDetails?.legalFirstName ?? '', + legalLastName: personalBankAccount?.legalLastName ?? privatePersonalDetails?.legalLastName ?? '', + addressStreet: personalBankAccount?.addressStreet ?? currentAddress?.addressLine1 ?? '', + addressCity: personalBankAccount?.addressCity ?? currentAddress?.city ?? '', + addressState: personalBankAccount?.addressState ?? currentAddress?.state ?? '', + addressZip: personalBankAccount?.addressZipCode ?? currentAddress?.zipCode ?? '', + }; + }, [personalBankAccount, privatePersonalDetails]); + + const submitBankAccountForm = useCallback(() => { + const bankAccounts = plaidData?.bankAccounts ?? []; + const policyID = personalBankAccount?.policyID; + const source = personalBankAccount?.source; + + const selectedPlaidBankAccount = bankAccounts.find((bankAccount) => bankAccount.plaidAccountID === personalBankAccount?.selectedPlaidAccountID); + + if (selectedPlaidBankAccount) { + const bankAccountWithToken = selectedPlaidBankAccount.plaidAccessToken + ? selectedPlaidBankAccount + : { + ...selectedPlaidBankAccount, + plaidAccessToken: plaidData?.plaidAccessToken ?? '', + }; + addPersonalBankAccount(bankAccountWithToken, policyID, source); + } + }, [plaidData, personalBankAccount]); + + const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(personalDetails), [personalDetails]); + + const { + componentToRender: SubStep, + isEditing, + nextScreen, + prevScreen, + moveTo, + screenIndex, + goToTheLastStep, + } = useSubStep({ + bodyContent, + startFrom, + onFinished: submitBankAccountForm, + }); + + const handleBackButtonPress = () => { + if (isEditing) { + goToTheLastStep(); + return; + } + if (screenIndex === 0) { + Navigation.goBack(); + return; + } + prevScreen(); + }; + + return ( + + + + ); +} + +PersonalInfoPage.displayName = 'PersonalInfoPage'; + +export default PersonalInfoPage; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx new file mode 100644 index 000000000000..acf87d3c11c4 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import CommonAddressStep from '@components/SubStepForms/AddressStep'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePersonalBankAccountDetailsFormSubmit from '@hooks/usePersonalBankAccountDetailsFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; + +const PERSONAL_INFO_STEP_KEY = INPUT_IDS.BANK_INFO_STEP; + +const INPUT_KEYS = { + street: PERSONAL_INFO_STEP_KEY.STREET, + city: PERSONAL_INFO_STEP_KEY.CITY, + state: PERSONAL_INFO_STEP_KEY.STATE, + zipCode: PERSONAL_INFO_STEP_KEY.ZIP_CODE, +}; + +const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.STREET, PERSONAL_INFO_STEP_KEY.CITY, PERSONAL_INFO_STEP_KEY.STATE, PERSONAL_INFO_STEP_KEY.ZIP_CODE]; + +function AddressStep({onNext, onMove, isEditing}: SubStepProps) { + const {translate} = useLocalize(); + + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const currentAddress = getCurrentAddress(privatePersonalDetails); + + const defaultValues = { + street: currentAddress?.addressLine1 ?? '', + city: currentAddress?.city ?? '', + state: currentAddress?.state ?? '', + zipCode: currentAddress?.zipCode ?? '', + }; + + const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + + isEditing={isEditing} + onNext={onNext} + onMove={onMove} + formID={ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM} + formTitle={translate('personalInfoStep.whatsYourAddress')} + formPOBoxDisclaimer={translate('common.noPO')} + onSubmit={handleSubmit} + stepFields={STEP_FIELDS} + inputFieldsIDs={INPUT_KEYS} + defaultValues={defaultValues} + /> + ); +} + +AddressStep.displayName = 'AddressStep'; + +export default AddressStep; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx new file mode 100644 index 000000000000..0a1c59ec795b --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx @@ -0,0 +1,83 @@ +import React, {useMemo} from 'react'; +import CommonConfirmationStep from '@components/SubStepForms/ConfirmationStep'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; + +const PERSONAL_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; +const PERSONAL_INFO_STEP_INDEXES = CONST.WALLET.PERSONAL_BANK_SUBSTEP_INDEXES; + +function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { + const {translate} = useLocalize(); + + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + + const isLoading = privatePersonalDetails?.isLoading ?? false; + const error = ErrorUtils.getLatestErrorMessage(privatePersonalDetails ?? {}); + + const personalDetails = useMemo(() => { + const currentAddress = getCurrentAddress(privatePersonalDetails); + const phone = bankAccountPersonalDetails?.phoneNumber ?? privatePersonalDetails?.phoneNumber; + return { + phoneNumber: (phone && parsePhoneNumber(phone, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', + legalFirstName: bankAccountPersonalDetails?.legalFirstName ?? privatePersonalDetails?.legalFirstName ?? '', + legalLastName: bankAccountPersonalDetails?.legalLastName ?? privatePersonalDetails?.legalLastName ?? '', + addressStreet: bankAccountPersonalDetails?.addressStreet ?? currentAddress?.addressLine1 ?? '', + addressCity: bankAccountPersonalDetails?.addressCity ?? currentAddress?.city ?? '', + addressState: bankAccountPersonalDetails?.addressState ?? currentAddress?.state ?? '', + addressZip: bankAccountPersonalDetails?.addressZipCode ?? currentAddress?.zipCode ?? '', + }; + }, [bankAccountPersonalDetails, privatePersonalDetails]); + + const summaryItems = [ + { + description: translate('personalInfoStep.legalName'), + title: `${personalDetails[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${personalDetails[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`, + shouldShowRightIcon: true, + onPress: () => { + onMove(PERSONAL_INFO_STEP_INDEXES.LEGAL_NAME); + }, + }, + { + description: translate('personalInfoStep.address'), + title: `${personalDetails?.addressStreet}, ${personalDetails?.addressCity}, ${personalDetails?.addressState} ${personalDetails?.addressZip}`, + shouldShowRightIcon: true, + onPress: () => { + onMove(PERSONAL_INFO_STEP_INDEXES.ADDRESS); + }, + }, + { + description: translate('common.phoneNumber'), + title: personalDetails[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER], + shouldShowRightIcon: true, + onPress: () => { + onMove(PERSONAL_INFO_STEP_INDEXES.PHONE_NUMBER); + }, + }, + ]; + + return ( + + ); +} + +ConfirmationStep.displayName = 'ConfirmationStep'; + +export default ConfirmationStep; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx new file mode 100644 index 000000000000..d027f886f97c --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import FullNameStep from '@components/SubStepForms/FullNameStep'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePersonalBankAccountDetailsFormSubmit from '@hooks/usePersonalBankAccountDetailsFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; + +const PERSONAL_INFO_STEP_KEY = INPUT_IDS.BANK_INFO_STEP; +const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.FIRST_NAME, PERSONAL_INFO_STEP_KEY.LAST_NAME]; + +function LegalNameStep({onNext, onMove, isEditing}: SubStepProps) { + const {translate} = useLocalize(); + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + + const defaultValues = { + firstName: privatePersonalDetails?.[PERSONAL_INFO_STEP_KEY.FIRST_NAME] ?? '', + lastName: privatePersonalDetails?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', + }; + + const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + + isEditing={isEditing} + onNext={onNext} + onMove={onMove} + formID={ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM} + formTitle={translate('personalInfoStep.whatsYourLegalName')} + onSubmit={handleSubmit} + stepFields={STEP_FIELDS} + firstNameInputID={PERSONAL_INFO_STEP_KEY.FIRST_NAME} + lastNameInputID={PERSONAL_INFO_STEP_KEY.LAST_NAME} + defaultValues={defaultValues} + /> + ); +} + +LegalNameStep.displayName = 'LegalNameStep'; + +export default LegalNameStep; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx new file mode 100644 index 000000000000..371557c1f250 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PhoneNumberStep.tsx @@ -0,0 +1,71 @@ +import React, {useCallback} from 'react'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import SingleFieldStep from '@components/SubStepForms/SingleFieldStep'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePersonalBankAccountDetailsFormSubmit from '@hooks/usePersonalBankAccountDetailsFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import {appendCountryCode, formatE164PhoneNumber} from '@libs/LoginUtils'; +import {getFieldRequiredErrors, isValidPhoneNumber, isValidUSPhone} from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; + +const PERSONAL_INFO_STEP_KEY = INPUT_IDS.BANK_INFO_STEP; +const STEP_FIELDS = [PERSONAL_INFO_STEP_KEY.PHONE_NUMBER]; + +function PhoneNumberStep({onNext, onMove, isEditing}: SubStepProps) { + const {translate} = useLocalize(); + + const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const defaultPhoneNumber = privatePersonalDetails?.[PERSONAL_INFO_STEP_KEY.PHONE_NUMBER] ?? ''; + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = getFieldRequiredErrors(values, STEP_FIELDS); + + if (values.phoneNumber) { + const phoneNumberWithCountryCode = appendCountryCode(values.phoneNumber, countryCode); + const e164FormattedPhoneNumber = formatE164PhoneNumber(values.phoneNumber, countryCode); + + if (!isValidPhoneNumber(phoneNumberWithCountryCode) || !isValidUSPhone(e164FormattedPhoneNumber)) { + errors.phoneNumber = translate('common.error.phoneNumber'); + } + } + + return errors; + }, + [countryCode, translate], + ); + + const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + + isEditing={isEditing} + onNext={onNext} + onMove={onMove} + formID={ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM} + formTitle={translate('personalInfoStep.whatsYourPhoneNumber')} + formDisclaimer={translate('personalInfoStep.weNeedThisToVerify')} + validate={validate} + onSubmit={(values) => { + handleSubmit({...values, phoneNumber: formatE164PhoneNumber(values.phoneNumber, countryCode) ?? ''}); + }} + inputId={PERSONAL_INFO_STEP_KEY.PHONE_NUMBER} + inputLabel={translate('common.phoneNumber')} + inputMode={CONST.INPUT_MODE.TEL} + defaultValue={defaultPhoneNumber} + enabledWhenOffline + /> + ); +} + +PhoneNumberStep.displayName = 'PhoneNumberStep'; + +export default PhoneNumberStep; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getInitialSubstepForPersonalInfo.ts b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getInitialSubstepForPersonalInfo.ts new file mode 100644 index 000000000000..b7c0b53343c0 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getInitialSubstepForPersonalInfo.ts @@ -0,0 +1,25 @@ +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import type {PersonalBankAccountForm} from '@src/types/form/PersonalBankAccountForm'; + +const personalInfoKeys = INPUT_IDS.BANK_INFO_STEP; + +/** + * Returns the initial substep for the Personal Info step based on already existing data + */ +function getInitialSubstepForPersonalInfo(data: Partial): number { + if (data[personalInfoKeys.FIRST_NAME] === '' || data[personalInfoKeys.LAST_NAME] === '') { + return 0; + } + + if (data[personalInfoKeys.STREET] === '' || data[personalInfoKeys.CITY] === '' || data[personalInfoKeys.STATE] === '' || data[personalInfoKeys.ZIP_CODE] === '') { + return 1; + } + + if (data[personalInfoKeys.PHONE_NUMBER] === '') { + return 2; + } + + return 3; +} + +export default getInitialSubstepForPersonalInfo; diff --git a/src/types/form/PersonalBankAccountForm.ts b/src/types/form/PersonalBankAccountForm.ts index 43c7945f1093..4340fa5c4317 100644 --- a/src/types/form/PersonalBankAccountForm.ts +++ b/src/types/form/PersonalBankAccountForm.ts @@ -12,6 +12,13 @@ const INPUT_IDS = { PLAID_ACCOUNT_ID: 'plaidAccountID', PLAID_ACCESS_TOKEN: 'plaidAccessToken', SELECTED_PLAID_ACCOUNT_ID: 'selectedPlaidAccountID', + FIRST_NAME: 'legalFirstName', + LAST_NAME: 'legalLastName', + STREET: 'addressStreet', + CITY: 'addressCity', + STATE: 'addressState', + ZIP_CODE: 'addressZipCode', + PHONE_NUMBER: 'phoneNumber', }, } as const; @@ -23,6 +30,13 @@ type BankAccountStepProps = { [INPUT_IDS.BANK_INFO_STEP.PLAID_ACCOUNT_ID]: string; [INPUT_IDS.BANK_INFO_STEP.PLAID_MASK]: string; [INPUT_IDS.BANK_INFO_STEP.SETUP_TYPE]: string; + [INPUT_IDS.BANK_INFO_STEP.FIRST_NAME]: string; + [INPUT_IDS.BANK_INFO_STEP.LAST_NAME]: string; + [INPUT_IDS.BANK_INFO_STEP.STREET]: string; + [INPUT_IDS.BANK_INFO_STEP.CITY]: string; + [INPUT_IDS.BANK_INFO_STEP.STATE]: string; + [INPUT_IDS.BANK_INFO_STEP.ZIP_CODE]: string; + [INPUT_IDS.BANK_INFO_STEP.PHONE_NUMBER]: string; }; type PlaidAccountProps = { @@ -35,7 +49,6 @@ type PlaidAccountProps = { type OnfidoStepProps = { isOnfidoSetupComplete: boolean; }; - type PersonalBankAccountForm = Form & OnfidoStepProps; export type {PersonalBankAccountForm}; From 4d388c29ac7ff7ecbb170c24ca93c9e7d232f9ef Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 14 Nov 2025 18:19:48 +0200 Subject: [PATCH 0010/1015] nit --- src/pages/AddPersonalBankAccountPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index 0bccf951a99d..fc1a2bd25515 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -20,7 +20,6 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import PersonalInfoPage from './settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo'; function AddPersonalBankAccountPage() { const styles = useThemeStyles(); From c2f323dad6c0459a5c89bf8744ce83a274ba5e0f Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 17 Nov 2025 12:56:16 +0100 Subject: [PATCH 0011/1015] Make SplitExpensePage use new SelectionList --- .../SelectionList/BaseSelectionList.tsx | 27 ++- .../SelectionList/ListItem/SplitListItem.tsx | 207 ++++++++++++++++++ .../SelectionList/ListItem/types.ts | 44 +++- src/components/SelectionList/types.ts | 3 + .../index.native.ts | 4 +- .../index.tsx | 2 +- .../types.ts | 4 +- src/pages/iou/SplitExpensePage.tsx | 42 ++-- 8 files changed, 302 insertions(+), 31 deletions(-) create mode 100644 src/components/SelectionList/ListItem/SplitListItem.tsx diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 60934952ad42..e9c78eeaead8 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -338,6 +338,26 @@ function BaseSelectionList({ } }; + // this function is used specifically for scrolling to the focused input to prevent it from appearing below opened keyboard + // and ensures the entire list item element is visible, not just the input field inside it + const scrollToFocusedInput = useCallback((index: number) => { + if (!listRef.current) { + return; + } + + if (index < 0) { + return; + } + + // Perform scroll to specific position in SectionList to show entire item + listRef.current.scrollToIndex({ + index: index + 2, // Scroll to item at index + 2 (because first two items is reserved for optional header and content above the selectionList) + animated: true, + viewOffset: 4, // scrollToLocation scrolls 4 pixels more than the specified list item, so we need to subtract this using viewOffset + viewPosition: 1.0, // Item position: 1.0 = bottom of screen + }); + }, []); + const scrollAndHighlightItem = useCallback( (items: string[]) => { const newItemsToHighlight = new Set(items); @@ -387,7 +407,12 @@ function BaseSelectionList({ } }, [onSelectAll, shouldShowTextInput, shouldPreventDefaultFocusOnSelectRow]); - useImperativeHandle(ref, () => ({scrollAndHighlightItem, scrollToIndex, updateFocusedIndex}), [scrollAndHighlightItem, scrollToIndex, updateFocusedIndex]); + useImperativeHandle(ref, () => ({scrollAndHighlightItem, scrollToIndex, updateFocusedIndex, scrollToFocusedInput}), [ + scrollAndHighlightItem, + scrollToIndex, + updateFocusedIndex, + scrollToFocusedInput, + ]); return ( {textInputComponent({shouldBeInsideList: false})} diff --git a/src/components/SelectionList/ListItem/SplitListItem.tsx b/src/components/SelectionList/ListItem/SplitListItem.tsx new file mode 100644 index 000000000000..e7427df5c345 --- /dev/null +++ b/src/components/SelectionList/ListItem/SplitListItem.tsx @@ -0,0 +1,207 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import {Folder, Tag} from '@components/Icon/Expensicons'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MoneyRequestAmountInput from '@components/MoneyRequestAmountInput'; +import type {ListItem} from '@components/SelectionList/types'; +import Text from '@components/Text'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getDecodedCategoryName} from '@libs/CategoryUtils'; +import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; +import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import BaseListItem from './BaseListItem'; +import type {SplitListItemProps, SplitListItemType} from './types'; + +function SplitListItem({ + item, + isFocused, + showTooltip, + isDisabled, + onSelectRow, + shouldPreventEnterKeySubmit, + rightHandSideComponent, + onFocus, + index, + onInputFocus, + onInputBlur, +}: SplitListItemProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + + const splitItem = item as unknown as SplitListItemType; + + const formattedOriginalAmount = convertToDisplayStringWithoutCurrency(splitItem.originalAmount, splitItem.currency); + + const onSplitExpenseAmountChange = (amount: string) => { + splitItem.onSplitExpenseAmountChange(splitItem.transactionID, Number(amount)); + }; + + const isBottomVisible = !!splitItem.category || !!splitItem.tags?.at(0); + + const [prefixCharacterMargin, setPrefixCharacterMargin] = useState(CONST.CHARACTER_WIDTH); + const inputMarginLeft = prefixCharacterMargin + styles.pl1.paddingLeft; + const contentWidth = (formattedOriginalAmount.length + 1) * CONST.CHARACTER_WIDTH; + const focusHandler = useCallback(() => { + if (!onInputFocus) { + return; + } + + if (!index && index !== 0) { + return; + } + onInputFocus(index); + }, [onInputFocus, index]); + + return ( + + + + + + + {splitItem.headerText} + + + + + + {splitItem.merchant} + + + + + {isBottomVisible && ( + + {!!splitItem.category && ( + + + + {getDecodedCategoryName(splitItem.category)} + + + )} + {!!splitItem.tags?.at(0) && ( + + + + {getCommaSeparatedTagNameWithSanitizedColons(splitItem.tags?.at(0) ?? '')} + + + )} + + )} + + + + {!splitItem.isEditable ? ( + + { + if (event.nativeEvent.layout.width === 0 && event.nativeEvent.layout.height === 0) { + return; + } + setPrefixCharacterMargin(event?.nativeEvent?.layout.width); + }} + > + {splitItem.currencySymbol} + + + {convertToDisplayStringWithoutCurrency(splitItem.amount, splitItem.currency)} + + + ) : ( + + )} + + + {!splitItem.isEditable ? null : ( + + + + )} + + + + + ); +} + +SplitListItem.displayName = 'SplitListItem'; + +export default SplitListItem; diff --git a/src/components/SelectionList/ListItem/types.ts b/src/components/SelectionList/ListItem/types.ts index 53fc743f1425..56d7a6fdca64 100644 --- a/src/components/SelectionList/ListItem/types.ts +++ b/src/components/SelectionList/ListItem/types.ts @@ -1,10 +1,11 @@ import type {ReactElement, ReactNode} from 'react'; -import type {AccessibilityState, NativeSyntheticEvent, StyleProp, TargetedEvent, TextStyle, ViewStyle} from 'react-native'; +import type {AccessibilityState, BlurEvent, NativeSyntheticEvent, StyleProp, TargetedEvent, TextStyle, ViewStyle} from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {ForwardedFSClassProps} from '@libs/Fullstory/types'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; // eslint-disable-next-line no-restricted-imports import type CursorStyles from '@styles/utils/cursor/types'; +import type {SplitExpense} from '@src/types/onyx/IOU'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type BaseListItem from './BaseListItem'; @@ -236,6 +237,15 @@ type ListItemProps = CommonListItemProps & { /** Whether to highlight the selected item */ shouldHighlightSelectedItem?: boolean; + + /** Index of the item in the list */ + index?: number; + + /** Callback when the input inside the item is focused (if input exists) */ + onInputFocus?: (index: number) => void; + + /** Callback when the input inside the item is blurred (if input exists) */ + onInputBlur?: (e: BlurEvent) => void; }; type ValidListItem = @@ -269,6 +279,36 @@ type BaseListItemProps = CommonListItemProps & { /** Whether to highlight the selected item */ shouldHighlightSelectedItem?: boolean; }; + +type SplitListItemType = ListItem & + SplitExpense & { + /** Item header text */ + headerText: string; + + /** Merchant or vendor name */ + merchant: string; + + /** Currency code */ + currency: string; + + /** ID of split expense */ + transactionID: string; + + /** Currency symbol */ + currencySymbol: string; + + /** Original amount before split */ + originalAmount: number; + + /** Indicates whether a split wasn't approved, paid etc. when report.statusNum < CONST.REPORT.STATUS_NUM.CLOSED */ + isEditable: boolean; + + /** Function for updating amount */ + onSplitExpenseAmountChange: (currentItemTransactionID: string, value: number) => void; + }; + +type SplitListItemProps = ListItemProps; + type RadioListItemProps = ListItemProps; type SingleSelectListItemProps = ListItemProps; @@ -310,4 +350,6 @@ export type { SpendCategorySelectorListItemProps, UserListItemProps, InviteMemberListItemProps, + SplitListItemType, + SplitListItemProps, }; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index eece8f12754c..e1405c87e4e9 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -208,6 +208,9 @@ type SelectionListHandle = { /** Updates the focused index and optionally scrolls to it */ updateFocusedIndex: (newFocusedIndex: number, shouldScroll?: boolean) => void; + + /** Scrolls to the focused input on SplitExpensePage */ + scrollToFocusedInput: (index: number) => void; }; type DataDetailsType = { diff --git a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.native.ts b/src/hooks/useDisplayFocusedInputUnderKeyboard/index.native.ts index 288ff88d8810..3d594ba703b9 100644 --- a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.native.ts +++ b/src/hooks/useDisplayFocusedInputUnderKeyboard/index.native.ts @@ -3,8 +3,8 @@ import type {View} from 'react-native'; import {Dimensions, Platform} from 'react-native'; import {useKeyboardHandler} from 'react-native-keyboard-controller'; import {useSharedValue} from 'react-native-reanimated'; -import SplitListItem from '@components/SelectionListWithSections/SplitListItem'; -import type {SelectionListHandle} from '@components/SelectionListWithSections/types'; +import SplitListItem from '@components/SelectionList/ListItem/SplitListItem'; +import type {SelectionListHandle} from '@components/SelectionList/types'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import {FOOTER_BOTTOM_MARGIN, MARGIN_FROM_INPUT_ANDROID, MARGIN_FROM_INPUT_IOS} from './const'; import type UseDisplayFocusedInputUnderKeyboardType from './types'; diff --git a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx b/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx index b330ae4c5364..38176d09be7c 100644 --- a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx +++ b/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {View} from 'react-native'; +import type {SelectionListHandle} from '@components/SelectionList/types'; import SplitListItemFocus from '@components/SelectionListWithSections/SplitListItem'; -import type {SelectionListHandle} from '@components/SelectionListWithSections/types'; import useDebouncedState from '@hooks/useDebouncedState'; import type UseDisplayFocusedInputUnderKeyboardType from './types'; diff --git a/src/hooks/useDisplayFocusedInputUnderKeyboard/types.ts b/src/hooks/useDisplayFocusedInputUnderKeyboard/types.ts index 197793c13666..7eddea93d5ee 100644 --- a/src/hooks/useDisplayFocusedInputUnderKeyboard/types.ts +++ b/src/hooks/useDisplayFocusedInputUnderKeyboard/types.ts @@ -1,6 +1,6 @@ import type {View} from 'react-native'; -import type SplitListItem from '@components/SelectionListWithSections/SplitListItem'; -import type {SelectionListHandle} from '@components/SelectionListWithSections/types'; +import type SplitListItem from '@components/SelectionList/ListItem/SplitListItem'; +import type {SelectionListHandle} from '@components/SelectionList/types'; type UseDisplayFocusedInputUnderKeyboardType = { listRef: React.RefObject; diff --git a/src/pages/iou/SplitExpensePage.tsx b/src/pages/iou/SplitExpensePage.tsx index 8540277eebc1..edd0bd9164f1 100644 --- a/src/pages/iou/SplitExpensePage.tsx +++ b/src/pages/iou/SplitExpensePage.tsx @@ -1,7 +1,7 @@ import {deepEqual} from 'fast-equals'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {InteractionManager, Keyboard, View} from 'react-native'; -import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'; +// import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; @@ -11,8 +11,8 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; import {useSearchContext} from '@components/Search/SearchContext'; -import SelectionList from '@components/SelectionListWithSections'; -import type {SectionListDataType, SplitListItemType} from '@components/SelectionListWithSections/types'; +import SelectionList from '@components/SelectionList'; +import type {SplitListItemType} from '@components/SelectionList/ListItem/types'; import useDisplayFocusedInputUnderKeyboard from '@hooks/useDisplayFocusedInputUnderKeyboard'; import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; import useLocalize from '@hooks/useLocalize'; @@ -55,7 +55,7 @@ type SplitExpensePageProps = PlatformStackScreenProps (item.translationPath ? translate(item.translationPath) : (item.text ?? '')), [translate]); - const [sections] = useMemo(() => { + const options = useMemo(() => { const dotSeparator: TranslationPathOrText = {text: ` ${CONST.DOT_SEPARATOR} `}; const isTransactionMadeWithCard = isManagedCardTransaction(transaction); const showCashOrCard: TranslationPathOrText = {translationPath: isTransactionMadeWithCard ? 'iou.card' : 'iou.cash'}; @@ -273,9 +273,7 @@ function SplitExpensePage({route}: SplitExpensePageProps) { }; }); - const newSections: Array> = [{data: items}]; - - return [newSections]; + return items; }, [ transaction, draftTransaction?.comment?.splitExpenses, @@ -289,6 +287,8 @@ function SplitExpensePage({route}: SplitExpensePageProps) { getTranslatedText, ]); + const initiallyFocusedOptionKey = useMemo(() => options.find((option) => option.transactionID === splitExpenseTransactionID)?.keyForList, [options, splitExpenseTransactionID]); + const listFooterContent = useMemo(() => { const shouldShowMakeSplitsEven = childTransactions.length === 0; return ( @@ -339,11 +339,6 @@ function SplitExpensePage({route}: SplitExpensePageProps) { ); }, [sumOfSplitExpenses, transactionDetailsAmount, translate, transactionDetails.currency, errorMessage, styles.ph1, styles.mb2, styles.w100, onSaveSplitExpense, footerRef]); - const initiallyFocusedOptionKey = useMemo( - () => sections.at(0)?.data.find((option) => option.transactionID === splitExpenseTransactionID)?.keyForList, - [sections, splitExpenseTransactionID], - ); - return ( ( - - )} + // renderScrollComponent={(props) => ( + // + // )} onSelectRow={(item) => { if (!item.isEditable) { setCannotBeEditedModalVisible(true); @@ -389,17 +384,16 @@ function SplitExpensePage({route}: SplitExpensePageProps) { }); }} ref={listRef} - sections={sections} - initiallyFocusedOptionKey={initiallyFocusedOptionKey} + data={options} + initiallyFocusedItemKey={initiallyFocusedOptionKey} ListItem={SplitListItem} - containerStyle={[styles.flexBasisAuto]} + style={{containerStyle: styles.flexBasisAuto}} footerContent={footerContent} listFooterContent={listFooterContent} disableKeyboardShortcuts shouldSingleExecuteRowSelect canSelectMultiple={false} shouldPreventDefaultFocusOnSelectRow - removeClippedSubviews={false} /> Date: Mon, 17 Nov 2025 19:05:46 +0700 Subject: [PATCH 0012/1015] Fix - Add payment card RHP opens on the Profile page instead of Subscription --- src/libs/Navigation/helpers/linkTo/index.ts | 23 +++++++-------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/libs/Navigation/helpers/linkTo/index.ts b/src/libs/Navigation/helpers/linkTo/index.ts index 535754f57409..73f750f6ec16 100644 --- a/src/libs/Navigation/helpers/linkTo/index.ts +++ b/src/libs/Navigation/helpers/linkTo/index.ts @@ -64,20 +64,6 @@ function isNavigatingToReportWithSameReportID(currentRoute: NavigationPartialRou return currentParams?.reportID === newParams?.reportID; } -function areFullScreenRoutesEqual(matchingFullScreenRoute: NavigationPartialRoute, lastFullScreenRoute: NavigationPartialRoute) { - const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); - const lastRouteInLastFullScreenRoute = lastFullScreenRoute.state?.routes?.at(-1); - - // We need to perform a manual check here, since it's possible to open the `WorkspaceRestrictedActionPage` via the FAB while still on the Settings page. - if (lastRouteInMatchingFullScreen?.name === SCREENS.SETTINGS.SUBSCRIPTION.ROOT && lastRouteInLastFullScreenRoute?.name !== SCREENS.SETTINGS.SUBSCRIPTION.ROOT) { - return false; - } - - const isEqualFullScreenRoute = matchingFullScreenRoute.name === lastFullScreenRoute.name; - - return isEqualFullScreenRoute; -} - function isRoutePreloaded(currentState: PlatformStackNavigationState, matchingFullScreenRoute: NavigationPartialRoute) { const lastRouteInMatchingFullScreen = matchingFullScreenRoute.state?.routes?.at(-1); @@ -156,8 +142,13 @@ export default function linkTo(navigation: NavigationContainerRef isFullScreenName(route.name)); - - if (matchingFullScreenRoute && lastFullScreenRoute && !areFullScreenRoutesEqual(matchingFullScreenRoute, lastFullScreenRoute as NavigationPartialRoute)) { + const lastRouteInLastFullScreenRoute = lastFullScreenRoute?.state?.routes.at(-1); + if ( + matchingFullScreenRoute && + lastFullScreenRoute && + (matchingFullScreenRoute.name !== lastFullScreenRoute.name || + (newFocusedRoute.name === SCREENS.SETTINGS.SUBSCRIPTION.ADD_PAYMENT_CARD && lastRouteInLastFullScreenRoute?.name !== SCREENS.SETTINGS.SUBSCRIPTION.ROOT)) + ) { if (isRoutePreloaded(currentState, matchingFullScreenRoute)) { navigation.dispatch(StackActions.push(matchingFullScreenRoute.name)); } else { From 2a38f6e19be910e151900ac60220b1e79827db13 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 17 Nov 2025 15:39:09 +0100 Subject: [PATCH 0013/1015] Fix importing icons --- .../SelectionList/BaseSelectionList.tsx | 4 ++-- .../SelectionList/ListItem/SplitListItem.tsx | 10 ++++----- .../index.tsx | 2 +- src/pages/iou/SplitExpensePage.tsx | 22 +++++++++++++++---- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index e9c78eeaead8..99a852308ffd 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -353,8 +353,8 @@ function BaseSelectionList({ listRef.current.scrollToIndex({ index: index + 2, // Scroll to item at index + 2 (because first two items is reserved for optional header and content above the selectionList) animated: true, - viewOffset: 4, // scrollToLocation scrolls 4 pixels more than the specified list item, so we need to subtract this using viewOffset - viewPosition: 1.0, // Item position: 1.0 = bottom of screen + // viewOffset: 4, // scrollToLocation scrolls 4 pixels more than the specified list item, so we need to subtract this using viewOffset + // viewPosition: 1.0, // Item position: 1.0 = bottom of screen }); }, []); diff --git a/src/components/SelectionList/ListItem/SplitListItem.tsx b/src/components/SelectionList/ListItem/SplitListItem.tsx index e7427df5c345..1cd3e45725cb 100644 --- a/src/components/SelectionList/ListItem/SplitListItem.tsx +++ b/src/components/SelectionList/ListItem/SplitListItem.tsx @@ -1,11 +1,10 @@ import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; -import {Folder, Tag} from '@components/Icon/Expensicons'; -import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestAmountInput from '@components/MoneyRequestAmountInput'; import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -35,6 +34,7 @@ function SplitListItem({ const StyleUtils = useStyleUtils(); const splitItem = item as unknown as SplitListItemType; + const expensifyIcons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'Folder', 'Tag'] as const); const formattedOriginalAmount = convertToDisplayStringWithoutCurrency(splitItem.originalAmount, splitItem.currency); @@ -104,7 +104,7 @@ function SplitListItem({ {!!splitItem.category && ( ({ {!!splitItem.tags?.at(0) && ( ({ {!splitItem.isEditable ? null : ( diff --git a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx b/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx index 38176d09be7c..215d21c1004d 100644 --- a/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx +++ b/src/hooks/useDisplayFocusedInputUnderKeyboard/index.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {View} from 'react-native'; +import SplitListItemFocus from '@components/SelectionList/ListItem/SplitListItem'; import type {SelectionListHandle} from '@components/SelectionList/types'; -import SplitListItemFocus from '@components/SelectionListWithSections/SplitListItem'; import useDebouncedState from '@hooks/useDebouncedState'; import type UseDisplayFocusedInputUnderKeyboardType from './types'; diff --git a/src/pages/iou/SplitExpensePage.tsx b/src/pages/iou/SplitExpensePage.tsx index edd0bd9164f1..663557100acc 100644 --- a/src/pages/iou/SplitExpensePage.tsx +++ b/src/pages/iou/SplitExpensePage.tsx @@ -7,7 +7,6 @@ import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; import {useSearchContext} from '@components/Search/SearchContext'; @@ -15,6 +14,7 @@ import SelectionList from '@components/SelectionList'; import type {SplitListItemType} from '@components/SelectionList/ListItem/types'; import useDisplayFocusedInputUnderKeyboard from '@hooks/useDisplayFocusedInputUnderKeyboard'; import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; @@ -56,6 +56,7 @@ function SplitExpensePage({route}: SplitExpensePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {listRef, viewRef, footerRef, scrollToFocusedInput, SplitListItem} = useDisplayFocusedInputUnderKeyboard(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['ArrowsLeftRight', 'Plus'] as const); const {reportID, transactionID, splitExpenseTransactionID, backTo} = route.params; @@ -296,20 +297,33 @@ function SplitExpensePage({route}: SplitExpensePageProps) { {shouldShowMakeSplitsEven && ( )} ); - }, [onAddSplitExpense, onMakeSplitsEven, translate, childTransactions, shouldUseNarrowLayout, styles.w100, styles.ph4, styles.flexColumn, styles.mt1, styles.mb3]); + }, [ + childTransactions.length, + styles.w100, + styles.flexColumn, + styles.mt1, + styles.mb3, + styles.ph4, + shouldUseNarrowLayout, + onAddSplitExpense, + translate, + expensifyIcons.Plus, + expensifyIcons.ArrowsLeftRight, + onMakeSplitsEven, + ]); const footerContent = useMemo(() => { const shouldShowWarningMessage = sumOfSplitExpenses < transactionDetailsAmount; From 22f44a1aa48d84a25da5b52e365027d451a09e16 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Mon, 17 Nov 2025 15:41:50 +0100 Subject: [PATCH 0014/1015] Remove old SplitListItem --- .../SplitListItem.tsx | 206 ------------------ .../SelectionListWithSections/types.ts | 33 +-- 2 files changed, 1 insertion(+), 238 deletions(-) delete mode 100644 src/components/SelectionListWithSections/SplitListItem.tsx diff --git a/src/components/SelectionListWithSections/SplitListItem.tsx b/src/components/SelectionListWithSections/SplitListItem.tsx deleted file mode 100644 index f5c26ed40955..000000000000 --- a/src/components/SelectionListWithSections/SplitListItem.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import React, {useCallback, useState} from 'react'; -import {View} from 'react-native'; -import Icon from '@components/Icon'; -import {Folder, Tag} from '@components/Icon/Expensicons'; -import * as Expensicons from '@components/Icon/Expensicons'; -import MoneyRequestAmountInput from '@components/MoneyRequestAmountInput'; -import Text from '@components/Text'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getDecodedCategoryName} from '@libs/CategoryUtils'; -import {convertToDisplayStringWithoutCurrency} from '@libs/CurrencyUtils'; -import {getCommaSeparatedTagNameWithSanitizedColons} from '@libs/PolicyUtils'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import BaseListItem from './BaseListItem'; -import type {ListItem, SplitListItemProps, SplitListItemType} from './types'; - -function SplitListItem({ - item, - isFocused, - showTooltip, - isDisabled, - onSelectRow, - shouldPreventEnterKeySubmit, - rightHandSideComponent, - onFocus, - index, - onInputFocus, - onInputBlur, -}: SplitListItemProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - - const splitItem = item as unknown as SplitListItemType; - - const formattedOriginalAmount = convertToDisplayStringWithoutCurrency(splitItem.originalAmount, splitItem.currency); - - const onSplitExpenseAmountChange = (amount: string) => { - splitItem.onSplitExpenseAmountChange(splitItem.transactionID, Number(amount)); - }; - - const isBottomVisible = !!splitItem.category || !!splitItem.tags?.at(0); - - const [prefixCharacterMargin, setPrefixCharacterMargin] = useState(CONST.CHARACTER_WIDTH); - const inputMarginLeft = prefixCharacterMargin + styles.pl1.paddingLeft; - const contentWidth = (formattedOriginalAmount.length + 1) * CONST.CHARACTER_WIDTH; - const focusHandler = useCallback(() => { - if (!onInputFocus) { - return; - } - - if (!index && index !== 0) { - return; - } - onInputFocus(index); - }, [onInputFocus, index]); - - return ( - - - - - - - {splitItem.headerText} - - - - - - {splitItem.merchant} - - - - - {isBottomVisible && ( - - {!!splitItem.category && ( - - - - {getDecodedCategoryName(splitItem.category)} - - - )} - {!!splitItem.tags?.at(0) && ( - - - - {getCommaSeparatedTagNameWithSanitizedColons(splitItem.tags?.at(0) ?? '')} - - - )} - - )} - - - - {!splitItem.isEditable ? ( - - { - if (event.nativeEvent.layout.width === 0 && event.nativeEvent.layout.height === 0) { - return; - } - setPrefixCharacterMargin(event?.nativeEvent?.layout.width); - }} - > - {splitItem.currencySymbol} - - - {convertToDisplayStringWithoutCurrency(splitItem.amount, splitItem.currency)} - - - ) : ( - - )} - - - {!splitItem.isEditable ? null : ( - - - - )} - - - - - ); -} - -SplitListItem.displayName = 'SplitListItem'; - -export default SplitListItem; diff --git a/src/components/SelectionListWithSections/types.ts b/src/components/SelectionListWithSections/types.ts index 567964974680..0504513e768d 100644 --- a/src/components/SelectionListWithSections/types.ts +++ b/src/components/SelectionListWithSections/types.ts @@ -26,7 +26,7 @@ import type CursorStyles from '@styles/utils/cursor/types'; import type {TransactionPreviewData} from '@userActions/Search'; import type CONST from '@src/CONST'; import type {PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, SearchResults, TransactionViolation, TransactionViolations} from '@src/types/onyx'; -import type {Attendee, SplitExpense} from '@src/types/onyx/IOU'; +import type {Attendee} from '@src/types/onyx/IOU'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type { SearchCardGroup, @@ -478,35 +478,6 @@ type UserListItemProps = ListItemProps & FooterComponent?: ReactElement; }; -type SplitListItemType = ListItem & - SplitExpense & { - /** Item header text */ - headerText: string; - - /** Merchant or vendor name */ - merchant: string; - - /** Currency code */ - currency: string; - - /** ID of split expense */ - transactionID: string; - - /** Currency symbol */ - currencySymbol: string; - - /** Original amount before split */ - originalAmount: number; - - /** Indicates whether a split wasn't approved, paid etc. when report.statusNum < CONST.REPORT.STATUS_NUM.CLOSED */ - isEditable: boolean; - - /** Function for updating amount */ - onSplitExpenseAmountChange: (currentItemTransactionID: string, value: number) => void; - }; - -type SplitListItemProps = ListItemProps; - type TransactionSelectionListItem = ListItemProps & Transaction; type InviteMemberListItemProps = UserListItemProps & { @@ -1050,8 +1021,6 @@ export type { ReportActionListItemType, ChatListItemProps, SortableColumnName, - SplitListItemProps, - SplitListItemType, SearchListItem, UnreportedExpenseListItemType, }; From 4d083448ecf7e20e8a75b092fa45831071565b80 Mon Sep 17 00:00:00 2001 From: Zuzanna Furtak Date: Tue, 18 Nov 2025 15:46:39 +0100 Subject: [PATCH 0015/1015] Try to fix scrolling --- .../SelectionList/BaseSelectionList.tsx | 8 ++++++-- src/components/SelectionList/types.ts | 7 +++++-- src/pages/iou/SplitExpensePage.tsx | 17 +++++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 99a852308ffd..9e136c553411 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -51,6 +51,7 @@ function BaseSelectionList({ listEmptyContent, listFooterContent, rightHandSideComponent, + renderScrollComponent, alternateNumberOfSupportedLines, selectedItems = CONST.EMPTY_ARRAY, style, @@ -353,8 +354,8 @@ function BaseSelectionList({ listRef.current.scrollToIndex({ index: index + 2, // Scroll to item at index + 2 (because first two items is reserved for optional header and content above the selectionList) animated: true, - // viewOffset: 4, // scrollToLocation scrolls 4 pixels more than the specified list item, so we need to subtract this using viewOffset - // viewPosition: 1.0, // Item position: 1.0 = bottom of screen + viewOffset: 4, // scrollToLocation scrolls 4 pixels more than the specified list item, so we need to subtract this using viewOffset + viewPosition: 1.0, // Item position: 1.0 = bottom of screen }); }, []); @@ -429,6 +430,7 @@ function BaseSelectionList({ shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} /> ({ style={style?.listStyle as ViewStyle} initialScrollIndex={initialFocusedIndex} onScrollBeginDrag={onScrollBeginDrag} + removeClippedSubviews + // maintainVisibleContentPosition={{disabled: true}} ListHeaderComponent={ <> {customListHeaderContent} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index e1405c87e4e9..14bf5841be53 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,5 +1,5 @@ -import type {ReactElement, RefObject} from 'react'; -import type {GestureResponderEvent, InputModeOptions, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {JSXElementConstructor, ReactElement, RefObject} from 'react'; +import type {GestureResponderEvent, InputModeOptions, ScrollViewProps, StyleProp, TextStyle, ViewStyle} from 'react-native'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import type {ListItem, ValidListItem} from './ListItem/types'; @@ -58,6 +58,9 @@ type SelectionListProps = { /** Component to display on the right side of each item */ rightHandSideComponent?: ((item: TItem, isFocused?: boolean) => ReactElement | null | undefined) | ReactElement | null; + /** Custom scroll component to use instead of the default ScrollView */ + renderScrollComponent?: (props: ScrollViewProps) => ReactElement>; + /** Number of lines to show for alternate text */ alternateNumberOfSupportedLines?: number; diff --git a/src/pages/iou/SplitExpensePage.tsx b/src/pages/iou/SplitExpensePage.tsx index 663557100acc..eb343a5acffe 100644 --- a/src/pages/iou/SplitExpensePage.tsx +++ b/src/pages/iou/SplitExpensePage.tsx @@ -1,6 +1,7 @@ import {deepEqual} from 'fast-equals'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {InteractionManager, Keyboard, View} from 'react-native'; +import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'; // import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import Button from '@components/Button'; @@ -55,7 +56,7 @@ type SplitExpensePageProps = PlatformStackScreenProps ( - // - // )} + renderScrollComponent={(props) => ( + + )} onSelectRow={(item) => { if (!item.isEditable) { setCannotBeEditedModalVisible(true); From 2558b4111f80589a29544732fd5a1b5ef14eb89e Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 19 Nov 2025 16:55:02 +0200 Subject: [PATCH 0016/1015] add plaid and manual steps for personal bank accounts --- src/pages/AddPersonalBankAccountPage.tsx | 75 +++----- .../PersonalInfo/PersonalInfo.tsx | 39 ++-- .../substeps/ConfirmationStep.tsx | 65 ++++--- .../substeps/ManualBankAccountDetailsStep.tsx | 106 +++++++++++ .../substeps/PlaidBankAccountStep.tsx | 60 ++++++ .../utils/getInitialSubstepForPersonalInfo.ts | 25 --- .../utils/getSkippedStepsPersonalInfo.ts | 25 +++ .../substeps/AccountFlowEntryPoint.tsx | 180 +++--------------- 8 files changed, 295 insertions(+), 280 deletions(-) create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ManualBankAccountDetailsStep.tsx create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PlaidBankAccountStep.tsx delete mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getInitialSubstepForPersonalInfo.ts create mode 100644 src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index fc1a2bd25515..04ee1ecd5435 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -1,9 +1,6 @@ -import React, {useCallback, useContext, useEffect, useState} from 'react'; -import AddPlaidBankAccount from '@components/AddPlaidBankAccount'; +import React, {useCallback, useContext, useEffect} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmationPage from '@components/ConfirmationPage'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {KYCWallContext} from '@components/KYCWall/KYCWallContext'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -11,22 +8,19 @@ import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; -import {clearPersonalBankAccount, validatePlaidSelection} from '@userActions/BankAccounts'; +import {clearPersonalBankAccount} from '@userActions/BankAccounts'; import {continueSetup} from '@userActions/PaymentMethods'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; +import PersonalInfoPage from './settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo'; function AddPersonalBankAccountPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [selectedPlaidAccountId, setSelectedPlaidAccountId] = useState(''); const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {canBeMissing: true}); - const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA, {canBeMissing: true}); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const topmostFullScreenRoute = navigationRef.current?.getRootState()?.routes.findLast((route) => isFullScreenName(route.name)); const kycWallRef = useContext(KYCWallContext); @@ -45,10 +39,6 @@ function AddPersonalBankAccountPage() { } }, [topmostFullScreenRoute?.name]); - const moveToPersonalStep = useCallback(() => { - // Add navigation to Personal info screens - }, []); - const exitFlow = useCallback( (shouldContinue = false) => { const exitReportID = personalBankAccount?.exitReportID; @@ -67,19 +57,19 @@ function AddPersonalBankAccountPage() { useEffect(() => clearPersonalBankAccount, []); - return ( - - - - {shouldShowSuccess ? ( + if (shouldShowSuccess) { + return ( + + + - ) : ( - 0} - submitButtonText={translate('common.saveAndContinue')} - scrollContextEnabled - onSubmit={moveToPersonalStep} - validate={validatePlaidSelection} - style={[styles.mh5, styles.flex1]} - shouldHideFixErrorsAlert - > - - - )} - - - ); + + + ); + } + + return ; } AddPersonalBankAccountPage.displayName = 'AddPersonalBankAccountPage'; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx index a4d94a646299..146a2ee1ff68 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx @@ -4,8 +4,6 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; -import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; -import {parsePhoneNumber} from '@libs/PhoneNumber'; import Navigation from '@navigation/Navigation'; import {addPersonalBankAccount} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; @@ -13,37 +11,26 @@ import ONYXKEYS from '@src/ONYXKEYS'; import Address from './substeps/AddressStep'; import Confirmation from './substeps/ConfirmationStep'; import LegalName from './substeps/LegalNameStep'; +import ManualBankAccountDetails from './substeps/ManualBankAccountDetailsStep'; import PhoneNumber from './substeps/PhoneNumberStep'; -import getInitialSubstepForPersonalInfo from './utils/getInitialSubstepForPersonalInfo'; +import PlaidBankAccount from './substeps/PlaidBankAccountStep'; +import getSkippedStepsPersonalInfo from './utils/getSkippedStepsPersonalInfo'; -const bodyContent: Array> = [LegalName, Address, PhoneNumber, Confirmation]; +const bodyContentInfoSet: Array> = [LegalName, Address, PhoneNumber, Confirmation]; +const bodyContentWithPlaid: Array> = [PlaidBankAccount, ...bodyContentInfoSet]; +const bodyContentWithManualSetup: Array> = [ManualBankAccountDetails, ...bodyContentInfoSet]; function PersonalInfoPage() { const {translate} = useLocalize(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); - const [personalBankAccount] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM); + const [personalBankAccount] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + const isManual = personalBankAccount?.setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL; const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA, {canBeMissing: true}); - const personalDetails = useMemo(() => { - const currentAddress = getCurrentAddress(privatePersonalDetails); - const phone = personalBankAccount?.phoneNumber ?? privatePersonalDetails?.phoneNumber; - return { - phoneNumber: (phone && parsePhoneNumber(phone, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', - legalFirstName: personalBankAccount?.legalFirstName ?? privatePersonalDetails?.legalFirstName ?? '', - legalLastName: personalBankAccount?.legalLastName ?? privatePersonalDetails?.legalLastName ?? '', - addressStreet: personalBankAccount?.addressStreet ?? currentAddress?.addressLine1 ?? '', - addressCity: personalBankAccount?.addressCity ?? currentAddress?.city ?? '', - addressState: personalBankAccount?.addressState ?? currentAddress?.state ?? '', - addressZip: personalBankAccount?.addressZipCode ?? currentAddress?.zipCode ?? '', - }; - }, [personalBankAccount, privatePersonalDetails]); - const submitBankAccountForm = useCallback(() => { const bankAccounts = plaidData?.bankAccounts ?? []; - const policyID = personalBankAccount?.policyID; - const source = personalBankAccount?.source; const selectedPlaidBankAccount = bankAccounts.find((bankAccount) => bankAccount.plaidAccountID === personalBankAccount?.selectedPlaidAccountID); @@ -54,11 +41,11 @@ function PersonalInfoPage() { ...selectedPlaidBankAccount, plaidAccessToken: plaidData?.plaidAccessToken ?? '', }; - addPersonalBankAccount(bankAccountWithToken, policyID, source); + addPersonalBankAccount(bankAccountWithToken); } }, [plaidData, personalBankAccount]); - const startFrom = useMemo(() => getInitialSubstepForPersonalInfo(personalDetails), [personalDetails]); + const skipSteps = useMemo(() => getSkippedStepsPersonalInfo(privatePersonalDetails), [privatePersonalDetails]); const { componentToRender: SubStep, @@ -69,8 +56,8 @@ function PersonalInfoPage() { screenIndex, goToTheLastStep, } = useSubStep({ - bodyContent, - startFrom, + bodyContent: isManual ? bodyContentWithManualSetup : bodyContentWithPlaid, + skipSteps, onFinished: submitBankAccountForm, }); @@ -91,7 +78,7 @@ function PersonalInfoPage() { wrapperID={PersonalInfoPage.displayName} headerTitle={translate('personalInfoStep.personalInfo')} handleBackButtonPress={handleBackButtonPress} - startStepIndex={1} + startStepIndex={0} stepNames={CONST.WALLET.STEP_NAMES} > { const currentAddress = getCurrentAddress(privatePersonalDetails); @@ -36,32 +39,44 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { }; }, [bankAccountPersonalDetails, privatePersonalDetails]); - const summaryItems = [ - { - description: translate('personalInfoStep.legalName'), - title: `${personalDetails[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${personalDetails[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`, - shouldShowRightIcon: true, - onPress: () => { - onMove(PERSONAL_INFO_STEP_INDEXES.LEGAL_NAME); + const summaryItems = useMemo(() => { + const selectedPlaidAccount = plaidData?.bankAccounts?.find((bankAccount) => bankAccount?.plaidAccountID === bankAccountPersonalDetails?.selectedPlaidAccountID); + + return [ + { + description: isManual ? translate('bankAccount.accountNumber') : translate('common.bankAccount'), + title: isManual ? bankAccountPersonalDetails?.accountNumber : (selectedPlaidAccount?.addressName ?? ''), + shouldShowRightIcon: true, + onPress: () => { + onMove(0); + }, + }, + { + description: translate('personalInfoStep.legalName'), + title: `${personalDetails[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${personalDetails[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`, + shouldShowRightIcon: true, + onPress: () => { + onMove(1); + }, }, - }, - { - description: translate('personalInfoStep.address'), - title: `${personalDetails?.addressStreet}, ${personalDetails?.addressCity}, ${personalDetails?.addressState} ${personalDetails?.addressZip}`, - shouldShowRightIcon: true, - onPress: () => { - onMove(PERSONAL_INFO_STEP_INDEXES.ADDRESS); + { + description: translate('personalInfoStep.address'), + title: `${personalDetails?.addressStreet}, ${personalDetails?.addressCity}, ${personalDetails?.addressState} ${personalDetails?.addressZip}`, + shouldShowRightIcon: true, + onPress: () => { + onMove(2); + }, }, - }, - { - description: translate('common.phoneNumber'), - title: personalDetails[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER], - shouldShowRightIcon: true, - onPress: () => { - onMove(PERSONAL_INFO_STEP_INDEXES.PHONE_NUMBER); + { + description: translate('common.phoneNumber'), + title: personalDetails[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER], + shouldShowRightIcon: true, + onPress: () => { + onMove(3); + }, }, - }, - ]; + ]; + }, [bankAccountPersonalDetails?.accountNumber, bankAccountPersonalDetails?.selectedPlaidAccountID, isManual, onMove, personalDetails, plaidData?.bankAccounts, translate]); return ( ({ + routingNumber: bankAccountPersonalDetails?.routingNumber, + accountNumber: bankAccountPersonalDetails?.accountNumber, + }), + [bankAccountPersonalDetails?.accountNumber, bankAccountPersonalDetails?.routingNumber], + ); + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = getFieldRequiredErrors(values, STEP_FIELDS); + const routingNumber = values.routingNumber?.trim(); + + if ( + values.accountNumber && + !CONST.BANK_ACCOUNT.REGEX.US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) && + !CONST.BANK_ACCOUNT.REGEX.MASKED_US_ACCOUNT_NUMBER.test(values.accountNumber.trim()) + ) { + errors.accountNumber = translate('bankAccount.error.accountNumber'); + } else if (values.accountNumber && values.accountNumber === routingNumber) { + errors.accountNumber = translate('bankAccount.error.routingAndAccountNumberCannotBeSame'); + } + if (routingNumber && (!CONST.BANK_ACCOUNT.REGEX.SWIFT_BIC.test(routingNumber) || !isValidRoutingNumber(routingNumber))) { + errors.routingNumber = translate('bankAccount.error.routingNumber'); + } + + return errors; + }, + [translate], + ); + + const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: true, + }); + + return ( + + {translate('bankAccount.manuallyAdd')} + {translate('bankAccount.checkHelpLine')} + + + + + ); +} + +ManualBankAccountDetailsStep.displayName = 'ManualBankAccountDetailsStep'; + +export default ManualBankAccountDetailsStep; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PlaidBankAccountStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PlaidBankAccountStep.tsx new file mode 100644 index 000000000000..b61596987298 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PlaidBankAccountStep.tsx @@ -0,0 +1,60 @@ +import React, {useState} from 'react'; +import AddPlaidBankAccount from '@components/AddPlaidBankAccount'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePersonalBankAccountDetailsFormSubmit from '@hooks/usePersonalBankAccountDetailsFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import getPlaidOAuthReceivedRedirectURI from '@libs/getPlaidOAuthReceivedRedirectURI'; +import Navigation from '@libs/Navigation/Navigation'; +import {validatePlaidSelection} from '@userActions/BankAccounts'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; + +const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; +const STEP_FIELDS = [BANK_INFO_STEP_KEYS.SELECTED_PLAID_ACCOUNT_ID]; + +function PlaidBankAccountStep({onNext, isEditing}: SubStepProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [selectedPlaidAccountId, setSelectedPlaidAccountId] = useState(''); + const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA, {canBeMissing: true}); + const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + + const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: true, + }); + + return ( + 0} + scrollContextEnabled + submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} + onSubmit={handleSubmit} + validate={validatePlaidSelection} + style={[styles.mh5, styles.flex1]} + shouldHideFixErrorsAlert + > + + + ); +} +PlaidBankAccountStep.displayName = 'PlaidBankAccountStep'; + +export default PlaidBankAccountStep; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getInitialSubstepForPersonalInfo.ts b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getInitialSubstepForPersonalInfo.ts deleted file mode 100644 index b7c0b53343c0..000000000000 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getInitialSubstepForPersonalInfo.ts +++ /dev/null @@ -1,25 +0,0 @@ -import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; -import type {PersonalBankAccountForm} from '@src/types/form/PersonalBankAccountForm'; - -const personalInfoKeys = INPUT_IDS.BANK_INFO_STEP; - -/** - * Returns the initial substep for the Personal Info step based on already existing data - */ -function getInitialSubstepForPersonalInfo(data: Partial): number { - if (data[personalInfoKeys.FIRST_NAME] === '' || data[personalInfoKeys.LAST_NAME] === '') { - return 0; - } - - if (data[personalInfoKeys.STREET] === '' || data[personalInfoKeys.CITY] === '' || data[personalInfoKeys.STATE] === '' || data[personalInfoKeys.ZIP_CODE] === '') { - return 1; - } - - if (data[personalInfoKeys.PHONE_NUMBER] === '') { - return 2; - } - - return 3; -} - -export default getInitialSubstepForPersonalInfo; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts new file mode 100644 index 000000000000..f9fe809bb3b0 --- /dev/null +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts @@ -0,0 +1,25 @@ +import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; +import type {PrivatePersonalDetails} from '@src/types/onyx'; + +/** + * Returns the initial substep for the Personal Info step based on already existing data + */ +function getSkippedStepsPersonalInfo(data?: Partial): number[] { + const currentAddress = getCurrentAddress(data); + const skippedSteps = []; + if (!!data?.legalFirstName && !!data?.legalLastName) { + skippedSteps.push(1); + } + + if (!!currentAddress?.addressLine1 && !!currentAddress?.city && currentAddress?.state && !!currentAddress?.zipCode) { + skippedSteps.push(2); + } + + if (data?.phoneNumber) { + skippedSteps.push(3); + } + + return skippedSteps; +} + +export default getSkippedStepsPersonalInfo; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx index f99dbbb864b5..824292d4ff6c 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx @@ -1,14 +1,10 @@ -import {isUserValidatedSelector} from '@selectors/Account'; -import React, {useCallback} from 'react'; +import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; -import {Bank, Connect, Lightbulb, Lock, RotateLeft} from '@components/Icon/Expensicons'; +import {Bank, Connect, Lightbulb, Lock} from '@components/Icon/Expensicons'; import LottieAnimations from '@components/LottieAnimations'; import MenuItem from '@components/MenuItem'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -20,124 +16,47 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getLatestError, getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import Navigation from '@navigation/Navigation'; -import WorkspaceResetBankAccountModal from '@pages/workspace/WorkspaceResetBankAccountModal'; -import {goToWithdrawalAccountSetupStep} from '@userActions/BankAccounts'; +import {updateAddPersonalBankAccountDraft} from '@userActions/BankAccounts'; import {openExternalLink} from '@userActions/Link'; -import {requestResetBankAccount, resetReimbursementAccount, setBankAccountSubStep, setReimbursementAccountOptionPressed} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -import type * as OnyxTypes from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AccountFlowEntryPointProps = { - /** Bank account currently in setup */ - reimbursementAccount: OnyxEntry; - - /** Callback to continue to the next step of the setup */ - onContinuePress: () => void; - /** The workspace name */ policyName?: string; - /** The workspace ID */ - policyID?: string; - /** Goes to the previous step */ onBackButtonPress: () => void; - - /** Should show the continue setup button */ - shouldShowContinueSetupButton: boolean | null; - - /** Whether the workspace currency is set to non USD currency */ - isNonUSDWorkspace: boolean; - - /** Should ValidateCodeActionModal be displayed or not */ - isValidateCodeActionModalVisible?: boolean; - - /** Toggle ValidateCodeActionModal */ - toggleValidateCodeActionModal?: (isVisible: boolean) => void; - - /** Set step for non USD flow */ - setNonUSDBankAccountStep: (shouldShowContinueSetupButton: string | null) => void; - - /** Set step for USD flow */ - setUSDBankAccountStep: (shouldShowContinueSetupButton: string | null) => void; - - /** Method to set the state of shouldShowContinueSetupButton */ - setShouldShowContinueSetupButton?: (shouldShowContinueSetupButton: boolean) => void; }; -const bankInfoStepKeys = INPUT_IDS.BANK_INFO_STEP; - -function AccountFlowEntryPoint({ - policyName = '', - onBackButtonPress, - reimbursementAccount, - onContinuePress, - shouldShowContinueSetupButton, - isNonUSDWorkspace, - isValidateCodeActionModalVisible, - toggleValidateCodeActionModal, - setNonUSDBankAccountStep, - setUSDBankAccountStep, - setShouldShowContinueSetupButton, -}: AccountFlowEntryPointProps) { +function AccountFlowEntryPoint({policyName = '', onBackButtonPress}: AccountFlowEntryPointProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector, canBeMissing: false}); - const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED, {canBeMissing: true}); - const errors = reimbursementAccount?.errors ?? {}; - const pendingAction = reimbursementAccount?.pendingAction ?? null; - const isAccountValidated = account?.validated ?? false; - - /** - * Prepares and redirects user to next step in the USD flow - */ - const prepareNextStep = useCallback( - (setupType: ValueOf) => { - setBankAccountSubStep(setupType); - setUSDBankAccountStep(CONST.BANK_ACCOUNT.STEP.COUNTRY); - goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.COUNTRY); - }, - [setUSDBankAccountStep], - ); const handleConnectManually = () => { - if (!isAccountValidated) { - setReimbursementAccountOptionPressed(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL); - toggleValidateCodeActionModal?.(true); - return; - } - - if (isNonUSDWorkspace) { - setNonUSDBankAccountStep(CONST.NON_USD_BANK_ACCOUNT.STEP.COUNTRY); - return; - } - - prepareNextStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL); + updateAddPersonalBankAccountDraft({ + setupType: CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL, + }); + Navigation.navigate(ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT); }; const handleConnectPlaid = () => { - if (isUserValidated) { - Navigation.navigate(ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT); - } else { - Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT); - } + updateAddPersonalBankAccountDraft({ + setupType: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID, + }); + Navigation.navigate(ROUTES.SETTINGS_ADD_US_BANK_ACCOUNT); }; return (
- {shouldShowContinueSetupButton === true ? ( - - - - - ) : ( - <> - - - - )} + +
@@ -236,16 +124,6 @@ function AccountFlowEntryPoint({ - - {!!reimbursementAccount?.shouldShowResetModal && ( - - )}
); } From b6a8b23035ff4cd34096ab48f5cd6c378474cdee Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Thu, 20 Nov 2025 13:06:33 +0530 Subject: [PATCH 0017/1015] add workspace feed cards for finding card name for money requests --- src/components/ReportActionItem/MoneyRequestView.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 4c5f22c9f363..9a9380819785 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -28,7 +28,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; import type {ViolationField} from '@hooks/useViolations'; import useViolations from '@hooks/useViolations'; -import {getCompanyCardDescription} from '@libs/CardUtils'; +import {getCompanyCardDescription, mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -166,6 +166,8 @@ function MoneyRequestView({ const allPolicyTags = usePolicyTags(); const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`]; const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); + const [companyCardList] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); + const mergedCardList = mergeCardListWithWorkspaceFeeds(companyCardList ?? CONST.EMPTY_OBJECT, cardList); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true}); const transactionViolations = useTransactionViolations(transaction?.transactionID); @@ -224,7 +226,7 @@ function MoneyRequestView({ const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = isCardTransactionTransactionUtils(transaction); - const cardProgramName = getCompanyCardDescription(transaction?.cardName, transaction?.cardID, cardList); + const cardProgramName = getCompanyCardDescription(transaction?.cardName, transaction?.cardID, mergedCardList); const shouldShowCard = isCardTransaction && cardProgramName; const taxRates = policy?.taxRates; From bd064e1efaaaf7a4f34a3bbc790fb789ff86c35e Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 20 Nov 2025 17:06:31 +0200 Subject: [PATCH 0018/1015] clean up manual and plaid flows --- src/CONST/index.ts | 5 -- .../PersonalInfo/PersonalInfo.tsx | 4 +- .../substeps/ConfirmationStep.tsx | 62 ++++++++++++++----- .../substeps/PlaidBankAccountStep.tsx | 2 +- .../substeps/AccountFlowEntryPoint.tsx | 11 ++-- 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ff561f7ece43..40b2fa1e6dca 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -2727,11 +2727,6 @@ const CONST = { PAY_SOMEONE: 'start/pay/manual', SPLIT_EXPENSE: 'start/split/manual', }, - PERSONAL_BANK_SUBSTEP_INDEXES: { - LEGAL_NAME: 0, - ADDRESS: 1, - PHONE_NUMBER: 2, - }, }, PLAID: { diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx index 146a2ee1ff68..b81429cb9df1 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx @@ -24,7 +24,7 @@ function PersonalInfoPage() { const {translate} = useLocalize(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); - const [personalBankAccount] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + const [personalBankAccount] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); const isManual = personalBankAccount?.setupType === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL; const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA, {canBeMissing: true}); @@ -78,8 +78,6 @@ function PersonalInfoPage() { wrapperID={PersonalInfoPage.displayName} headerTitle={translate('personalInfoStep.personalInfo')} handleBackButtonPress={handleBackButtonPress} - startStepIndex={0} - stepNames={CONST.WALLET.STEP_NAMES} > { const currentAddress = getCurrentAddress(privatePersonalDetails); @@ -41,16 +43,38 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { const summaryItems = useMemo(() => { const selectedPlaidAccount = plaidData?.bankAccounts?.find((bankAccount) => bankAccount?.plaidAccountID === bankAccountPersonalDetails?.selectedPlaidAccountID); + const bankConnection = isManual + ? [ + { + description: translate('bankAccount.routingNumber'), + title: bankAccountPersonalDetails?.routingNumber, + shouldShowRightIcon: true, + onPress: () => { + onMove(0); + }, + }, + { + description: translate('bankAccount.accountNumber'), + title: bankAccountPersonalDetails?.accountNumber, + shouldShowRightIcon: true, + onPress: () => { + onMove(0); + }, + }, + ] + : [ + { + description: translate('common.bankAccount'), + title: selectedPlaidAccount?.addressName ?? '', + shouldShowRightIcon: true, + onPress: () => { + onMove(0); + }, + }, + ]; return [ - { - description: isManual ? translate('bankAccount.accountNumber') : translate('common.bankAccount'), - title: isManual ? bankAccountPersonalDetails?.accountNumber : (selectedPlaidAccount?.addressName ?? ''), - shouldShowRightIcon: true, - onPress: () => { - onMove(0); - }, - }, + ...bankConnection, { description: translate('personalInfoStep.legalName'), title: `${personalDetails[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${personalDetails[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`, @@ -76,17 +100,25 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { }, }, ]; - }, [bankAccountPersonalDetails?.accountNumber, bankAccountPersonalDetails?.selectedPlaidAccountID, isManual, onMove, personalDetails, plaidData?.bankAccounts, translate]); + }, [ + bankAccountPersonalDetails?.accountNumber, + bankAccountPersonalDetails?.routingNumber, + bankAccountPersonalDetails?.selectedPlaidAccountID, + isManual, + onMove, + personalDetails, + plaidData?.bankAccounts, + translate, + ]); return ( diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PlaidBankAccountStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PlaidBankAccountStep.tsx index b61596987298..65f5f90345e5 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PlaidBankAccountStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/PlaidBankAccountStep.tsx @@ -21,7 +21,7 @@ function PlaidBankAccountStep({onNext, isEditing}: SubStepProps) { const {translate} = useLocalize(); const [selectedPlaidAccountId, setSelectedPlaidAccountId] = useState(''); const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA, {canBeMissing: true}); - const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT); + const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ fieldIds: STEP_FIELDS, diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx index 824292d4ff6c..a2b23e43f804 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/substeps/AccountFlowEntryPoint.tsx @@ -2,7 +2,6 @@ import React from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; -import {Bank, Connect, Lightbulb, Lock} from '@components/Icon/Expensicons'; import LottieAnimations from '@components/LottieAnimations'; import MenuItem from '@components/MenuItem'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -11,6 +10,7 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -36,6 +36,7 @@ function AccountFlowEntryPoint({policyName = '', onBackButtonPress}: AccountFlow const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['Bank', 'Connect', 'Lightbulb', 'Lock'] as const); const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED, {canBeMissing: true}); @@ -77,7 +78,7 @@ function AccountFlowEntryPoint({policyName = '', onBackButtonPress}: AccountFlow > {translate('bankAccount.yourDataIsSecure')} From 325936901cbc8334816038939df18518cb2c6849 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 21 Nov 2025 12:26:33 +0200 Subject: [PATCH 0019/1015] translations --- src/languages/de.ts | 1 + src/languages/en.ts | 2 +- src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 10 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 2ed9e23b7646..4dee0de31909 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2959,6 +2959,7 @@ ${ currencyHeader: 'Was ist die Währung Ihres Bankkontos?', confirmationStepHeader: 'Überprüfen Sie Ihre Informationen.', confirmationStepSubHeader: 'Überprüfen Sie die unten stehenden Details und aktivieren Sie das Kontrollkästchen für die Bedingungen, um zu bestätigen.', + toGetStarted: 'Fügen Sie ein persönliches Bankkonto hinzu, um Erstattungen zu erhalten, Rechnungen zu bezahlen oder die Expensify Wallet zu aktivieren.', }, addPersonalBankAccountPage: { enterPassword: 'Expensify-Passwort eingeben', diff --git a/src/languages/en.ts b/src/languages/en.ts index 6e3a310e670b..2dc275d32033 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3053,7 +3053,7 @@ const translations = { currencyHeader: "What's your bank account's currency?", confirmationStepHeader: 'Check your info.', confirmationStepSubHeader: 'Double check the details below, and check the terms box to confirm.', - toGetStarted: 'Add a personal bank account to receive reimbursements, pay invoices or enable the Expensify Wallet.', + toGetStarted: 'Add a personal bank account to receive reimbursements, pay invoices, or enable the Expensify Wallet.', }, addPersonalBankAccountPage: { enterPassword: 'Enter Expensify password', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4095856ccbb3..2b9415cfbd6c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2702,6 +2702,7 @@ ${amount} para ${merchant} - ${date}`, currencyHeader: '¿Cuál es la moneda de tu cuenta bancaria?', confirmationStepHeader: 'Verifica tu información.', confirmationStepSubHeader: 'Verifica dos veces los detalles a continuación y marca la casilla de términos para confirmar.', + toGetStarted: 'Agrega una cuenta bancaria personal para recibir reembolsos, pagar facturas o habilitar la Cartera de Expensify.', }, addPersonalBankAccountPage: { enterPassword: 'Escribe tu contraseña de Expensify', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 558c64d73d58..92a91fa90b94 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2960,6 +2960,7 @@ ${ currencyHeader: 'Quelle est la devise de votre compte bancaire ?', confirmationStepHeader: 'Vérifiez vos informations.', confirmationStepSubHeader: 'Vérifiez les détails ci-dessous et cochez la case des conditions pour confirmer.', + toGetStarted: 'Ajoutez un compte bancaire personnel pour recevoir des remboursements, payer des factures ou activer le portefeuille Expensify.', }, addPersonalBankAccountPage: { enterPassword: 'Entrez le mot de passe Expensify', diff --git a/src/languages/it.ts b/src/languages/it.ts index 7e1f9ad4ae89..29801aee71e9 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2942,6 +2942,7 @@ ${ currencyHeader: 'Qual è la valuta del tuo conto bancario?', confirmationStepHeader: 'Controlla le tue informazioni.', confirmationStepSubHeader: 'Ricontrolla i dettagli qui sotto e seleziona la casella dei termini per confermare.', + toGetStarted: 'Aggiungi un conto bancario personale per ricevere rimborsi, pagare fatture o abilitare il portafoglio Expensify.', }, addPersonalBankAccountPage: { enterPassword: 'Inserisci la password di Expensify', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c656c5f7cd52..e841abe8512a 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2931,6 +2931,7 @@ ${ currencyHeader: 'あなたの銀行口座の通貨は何ですか?', confirmationStepHeader: '情報を確認してください。', confirmationStepSubHeader: '以下の詳細を再確認し、利用規約のボックスをチェックして確認してください。', + toGetStarted: '払い戻しを受け取ったり、請求書を支払ったり、Expensify Wallet を有効にしたりするには、個人の銀行口座を追加します。', }, addPersonalBankAccountPage: { enterPassword: 'Expensifyのパスワードを入力してください', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 38820abc49c0..0902eb396e70 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2943,6 +2943,7 @@ ${ currencyHeader: 'Wat is de valuta van uw bankrekening?', confirmationStepHeader: 'Controleer uw gegevens.', confirmationStepSubHeader: 'Controleer de onderstaande gegevens en vink het vakje met de voorwaarden aan om te bevestigen.', + toGetStarted: 'Voeg een persoonlijke bankrekening toe om vergoedingen te ontvangen, facturen te betalen of de Expensify Wallet in te schakelen.', }, addPersonalBankAccountPage: { enterPassword: 'Voer Expensify-wachtwoord in', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 0134fdca0078..37be45108a11 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2937,6 +2937,7 @@ ${ currencyHeader: 'Jaka jest waluta Twojego konta bankowego?', confirmationStepHeader: 'Sprawdź swoje informacje.', confirmationStepSubHeader: 'Sprawdź poniższe szczegóły i zaznacz pole z warunkami, aby potwierdzić.', + toGetStarted: 'Dodaj osobiste konto bankowe, aby otrzymywać zwroty kosztów, opłacać faktury lub włączyć portfel Expensify.', }, addPersonalBankAccountPage: { enterPassword: 'Wprowadź hasło do Expensify', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 262a32428d11..ce5ad1519e38 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2938,6 +2938,7 @@ ${ currencyHeader: 'Qual é a moeda da sua conta bancária?', confirmationStepHeader: 'Verifique suas informações.', confirmationStepSubHeader: 'Verifique os detalhes abaixo e marque a caixa de termos para confirmar.', + toGetStarted: 'Adicione uma conta bancária pessoal para receber reembolsos, pagar faturas ou ativar a Carteira Expensify.', }, addPersonalBankAccountPage: { enterPassword: 'Digite a senha do Expensify', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index a0f83fdf35d4..2981484c9efb 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2899,6 +2899,7 @@ ${ currencyHeader: '您的银行账户货币是什么?', confirmationStepHeader: '检查您的信息。', confirmationStepSubHeader: '请仔细核对以下详细信息,并勾选条款框以确认。', + toGetStarted: '添加个人银行账户以接收报销、支付发票或启用 Expensify 钱包。', }, addPersonalBankAccountPage: { enterPassword: '输入Expensify密码', From b4f2e9dde137ced674d9dc9c015f726b74f522bc Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 21 Nov 2025 17:41:17 +0200 Subject: [PATCH 0020/1015] correct data passing for api call --- .../AddPersonalBankAccountParams.ts | 17 ++++++++---- src/libs/actions/BankAccounts.ts | 26 ++++++++++++++----- .../PersonalInfo/PersonalInfo.tsx | 19 ++++++-------- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/libs/API/parameters/AddPersonalBankAccountParams.ts b/src/libs/API/parameters/AddPersonalBankAccountParams.ts index 4fb571b3012e..628c6b2fefd0 100644 --- a/src/libs/API/parameters/AddPersonalBankAccountParams.ts +++ b/src/libs/API/parameters/AddPersonalBankAccountParams.ts @@ -1,14 +1,21 @@ type AddPersonalBankAccountParams = { addressName?: string; - routingNumber: string; - accountNumber: string; + routingNumber?: string; + accountNumber?: string; isSavings?: boolean; - setupType: string; + setupType?: string; bank?: string; - plaidAccountID: string; - plaidAccessToken: string; + plaidAccountID?: string; + plaidAccessToken?: string; policyID?: string; source?: string; + phoneNumber?: string; + legalFirstName?: string; + legalLastName?: string; + addressStreet?: string; + addressCity?: string; + addressState?: string; + addressZip?: string; }; export default AddPersonalBankAccountParams; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 01d686632d1d..34e22c9e346b 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -264,16 +264,28 @@ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAcc * * TODO: offline pattern for this command will have to be added later once the pattern B design doc is complete */ -function addPersonalBankAccount(account: PlaidBankAccount, policyID?: string, source?: string, lastPaymentMethod?: LastPaymentMethodType | string | undefined) { +function addPersonalBankAccount( + account: Partial, + policyID?: string, + source?: string, + lastPaymentMethod?: LastPaymentMethodType | string | undefined, +) { const parameters: AddPersonalBankAccountParams = { addressName: account.addressName ?? '', - routingNumber: account.routingNumber, - accountNumber: account.accountNumber, + routingNumber: account?.routingNumber, + accountNumber: account?.accountNumber, isSavings: account.isSavings ?? false, - setupType: 'plaid', - bank: account.bankName, - plaidAccountID: account.plaidAccountID, - plaidAccessToken: account.plaidAccessToken, + setupType: account?.setupType, + bank: account?.bankName, + plaidAccountID: account?.plaidAccountID, + plaidAccessToken: account?.plaidAccessToken, + phoneNumber: account?.phoneNumber, + legalFirstName: account?.legalFirstName, + legalLastName: account?.legalLastName, + addressStreet: account?.addressStreet, + addressCity: account?.addressCity, + addressState: account?.addressState, + addressZip: account?.addressZipCode, }; if (policyID) { parameters.policyID = policyID; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx index b81429cb9df1..221b0d590d01 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx @@ -33,16 +33,13 @@ function PersonalInfoPage() { const bankAccounts = plaidData?.bankAccounts ?? []; const selectedPlaidBankAccount = bankAccounts.find((bankAccount) => bankAccount.plaidAccountID === personalBankAccount?.selectedPlaidAccountID); - - if (selectedPlaidBankAccount) { - const bankAccountWithToken = selectedPlaidBankAccount.plaidAccessToken - ? selectedPlaidBankAccount - : { - ...selectedPlaidBankAccount, - plaidAccessToken: plaidData?.plaidAccessToken ?? '', - }; - addPersonalBankAccount(bankAccountWithToken); - } + const bankAccountWithToken = selectedPlaidBankAccount?.plaidAccessToken + ? selectedPlaidBankAccount + : { + ...selectedPlaidBankAccount, + plaidAccessToken: plaidData?.plaidAccessToken ?? '', + }; + addPersonalBankAccount({...personalBankAccount, ...bankAccountWithToken}); }, [plaidData, personalBankAccount]); const skipSteps = useMemo(() => getSkippedStepsPersonalInfo(privatePersonalDetails), [privatePersonalDetails]); @@ -76,7 +73,7 @@ function PersonalInfoPage() { return ( Date: Tue, 25 Nov 2025 03:28:23 +0800 Subject: [PATCH 0021/1015] fix: allow to delete formula after deleting it from settings --- .../MoneyRequestViewReportFields.tsx | 4 +--- src/components/ReportActionItem/MoneyReportView.tsx | 4 +--- src/libs/ReportUtils.ts | 6 +++++- src/pages/EditReportFieldPage.tsx | 11 +++++++++++ 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 8cee588e0fdb..0b6ddad88b40 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -113,9 +113,7 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, }); }, [policy, report, violations]); - const enabledReportFields = sortedPolicyReportFields.filter( - (reportField) => !isReportFieldDisabled(report, reportField, policy) || reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA, - ); + const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !isReportFieldDisabled(report, reportField, policy)); const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 67fb7fa10e5f..5309bd092134 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -93,9 +93,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo return fields.filter((field) => field.target === report?.type).sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); }, [policy, report]); - const enabledReportFields = sortedPolicyReportFields.filter( - (reportField) => !isReportFieldDisabled(report, reportField, policy) || reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA, - ); + const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !isReportFieldDisabled(report, reportField, policy)); const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); const isClosedExpenseReportWithNoExpenses = isClosedExpenseReportWithNoExpensesReportUtils(report); const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 90571ffb883e..6a8fd0faf7fc 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4267,7 +4267,11 @@ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry return !reportField?.deletable; } - return reportField?.type === CONST.REPORT_FIELD_TYPES.FORMULA; + if (reportField?.type === CONST.REPORT_FIELD_TYPES.FORMULA) { + return !reportField?.deletable; + } + + return false; } /** diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 12528ef9021c..6a0541235141 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -180,6 +180,17 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { onSubmit={handleReportFieldChange} /> )} + + {reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA && ( + + )} ); } From fa3db2c2c0c2b9a9aade64b3fe12cd5433158d68 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Tue, 25 Nov 2025 16:46:58 +0430 Subject: [PATCH 0022/1015] fix: show tag validation error when moving expenses between workspaces --- src/libs/Violations/ViolationsUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 94e552a3d10c..f78eba67a1a9 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -39,7 +39,7 @@ function getTagViolationsForSingleLevelTags( if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) { const tagName = policyTagList[policyTagListName]?.name; const tagNameToShow = isDefaultTagName(tagName) ? undefined : tagName; - newTransactionViolations.push({name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION, data: {tagName: tagNameToShow}}); + newTransactionViolations.push({name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION, data: {tagName: tagNameToShow}, showInReview: true}); } // Remove 'tagOutOfPolicy' violation if tag is in policy @@ -115,6 +115,7 @@ function getTagViolationForIndependentTags(policyTagList: PolicyTagLists, transa newTransactionViolations.push({ name: CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, type: CONST.VIOLATION_TYPES.VIOLATION, + showInReview: true, data: { errorIndexes, }, @@ -129,12 +130,12 @@ function getTagViolationForIndependentTags(policyTagList: PolicyTagLists, transa newTransactionViolations.push({ name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION, + showInReview: true, data: { tagName: policyTagKeys.at(i), }, }); hasInvalidTag = true; - break; } } if (!hasInvalidTag) { @@ -289,7 +290,7 @@ const ViolationsUtils = { } // Calculate client-side tag violations - const policyRequiresTags = !!policy.requiresTag && !isSelfDM; + const policyRequiresTags = (!!policy.requiresTag || !!updatedTransaction?.tag) && !isSelfDM; if (policyRequiresTags) { newTransactionViolations = Object.keys(policyTagList).length === 1 From adad44dfb4bb056d78e1aea2e34b1ea82ebe0574 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Tue, 25 Nov 2025 14:46:27 +0200 Subject: [PATCH 0023/1015] add suggestion for perf --- .../PersonalInfo/PersonalInfo.tsx | 6 +++++- .../PersonalInfo/substeps/ConfirmationStep.tsx | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx index 221b0d590d01..1ab6054cd825 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/PersonalInfo.tsx @@ -39,7 +39,11 @@ function PersonalInfoPage() { ...selectedPlaidBankAccount, plaidAccessToken: plaidData?.plaidAccessToken ?? '', }; - addPersonalBankAccount({...personalBankAccount, ...bankAccountWithToken}); + const accountData = { + ...personalBankAccount, + ...bankAccountWithToken, + }; + addPersonalBankAccount(accountData); }, [plaidData, personalBankAccount]); const skipSteps = useMemo(() => getSkippedStepsPersonalInfo(privatePersonalDetails), [privatePersonalDetails]); diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx index 6288794c816f..cf8f3d103307 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx @@ -39,7 +39,16 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { addressState: bankAccountPersonalDetails?.addressState ?? currentAddress?.state ?? '', addressZip: bankAccountPersonalDetails?.addressZipCode ?? currentAddress?.zipCode ?? '', }; - }, [bankAccountPersonalDetails, privatePersonalDetails]); + }, [ + bankAccountPersonalDetails?.addressCity, + bankAccountPersonalDetails?.addressState, + bankAccountPersonalDetails?.addressStreet, + bankAccountPersonalDetails?.addressZipCode, + bankAccountPersonalDetails?.legalFirstName, + bankAccountPersonalDetails?.legalLastName, + bankAccountPersonalDetails?.phoneNumber, + privatePersonalDetails, + ]); const summaryItems = useMemo(() => { const selectedPlaidAccount = plaidData?.bankAccounts?.find((bankAccount) => bankAccount?.plaidAccountID === bankAccountPersonalDetails?.selectedPlaidAccountID); From 65eaa556c9cb8bb8b630ce638dfcc1bb7dde60d8 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Tue, 25 Nov 2025 17:58:34 +0430 Subject: [PATCH 0024/1015] fixed unit test failure --- tests/unit/ViolationUtilsTest.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 9bf311d71c63..8a4dc062f4d6 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -79,6 +79,7 @@ const missingTagViolation = { const tagOutOfPolicyViolation = { name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION, + showInReview: true, }; const smartScanFailedViolation = { @@ -518,6 +519,7 @@ describe('getViolationsOnyxData', () => { const someTagLevelsRequiredViolation = { name: 'someTagLevelsRequired', type: CONST.VIOLATION_TYPES.VIOLATION, + showInReview: true, data: { errorIndexes: [0, 1, 2], }, From 8edefc6e6c76dfaaf25d14fc531e29cbe92bd8c9 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 26 Nov 2025 13:06:25 +0200 Subject: [PATCH 0025/1015] update code after c+ review --- src/libs/actions/BankAccounts.ts | 5 +++ .../PersonalInfo/substeps/AddressStep.tsx | 4 +-- .../substeps/ConfirmationStep.tsx | 31 +++++++++++++------ .../substeps/ManualBankAccountDetailsStep.tsx | 4 +-- .../utils/getSkippedStepsPersonalInfo.ts | 2 +- 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 34e22c9e346b..df82a7c95558 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -140,6 +140,10 @@ function clearPersonalBankAccountSetupType() { Onyx.merge(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {setupType: null}); } +function clearPersonalBankAccountErrors() { + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); +} + /** * Whether after adding a bank account we should continue with the KYC flow. If so, we must specify the fallback route. */ @@ -1349,6 +1353,7 @@ export { clearReimbursementAccount, clearEnterSignerInformationFormSave, sendReminderForCorpaySignerInformation, + clearPersonalBankAccountErrors, clearReimbursementAccountSendReminderForCorpaySignerInformation, getBankAccountFromID, }; diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx index acf87d3c11c4..c08d5496e0ec 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx @@ -26,10 +26,10 @@ function AddressStep({onNext, onMove, isEditing}: SubStepProps) { const currentAddress = getCurrentAddress(privatePersonalDetails); const defaultValues = { - street: currentAddress?.addressLine1 ?? '', + street: currentAddress?.street ?? '', city: currentAddress?.city ?? '', state: currentAddress?.state ?? '', - zipCode: currentAddress?.zipCode ?? '', + zipCode: currentAddress?.zip ?? '', }; const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx index cf8f3d103307..1aff76d0a18e 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ConfirmationStep.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import CommonConfirmationStep from '@components/SubStepForms/ConfirmationStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -6,6 +6,7 @@ import type {SubStepProps} from '@hooks/useSubStep/types'; import {getLatestErrorMessage} from '@libs/ErrorUtils'; import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; +import {clearPersonalBankAccountErrors} from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/PersonalBankAccountForm'; @@ -34,10 +35,10 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { phoneNumber: (phone && parsePhoneNumber(phone, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', legalFirstName: bankAccountPersonalDetails?.legalFirstName ?? privatePersonalDetails?.legalFirstName ?? '', legalLastName: bankAccountPersonalDetails?.legalLastName ?? privatePersonalDetails?.legalLastName ?? '', - addressStreet: bankAccountPersonalDetails?.addressStreet ?? currentAddress?.addressLine1 ?? '', + addressStreet: bankAccountPersonalDetails?.addressStreet ?? currentAddress?.street ?? '', addressCity: bankAccountPersonalDetails?.addressCity ?? currentAddress?.city ?? '', addressState: bankAccountPersonalDetails?.addressState ?? currentAddress?.state ?? '', - addressZip: bankAccountPersonalDetails?.addressZipCode ?? currentAddress?.zipCode ?? '', + addressZip: bankAccountPersonalDetails?.addressZipCode ?? currentAddress?.zip ?? '', }; }, [ bankAccountPersonalDetails?.addressCity, @@ -50,6 +51,16 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { privatePersonalDetails, ]); + const moveToEditStep = useCallback( + (step: number) => { + if (error) { + clearPersonalBankAccountErrors(); + } + onMove(step); + }, + [error, onMove], + ); + const summaryItems = useMemo(() => { const selectedPlaidAccount = plaidData?.bankAccounts?.find((bankAccount) => bankAccount?.plaidAccountID === bankAccountPersonalDetails?.selectedPlaidAccountID); const bankConnection = isManual @@ -59,7 +70,7 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { title: bankAccountPersonalDetails?.routingNumber, shouldShowRightIcon: true, onPress: () => { - onMove(0); + moveToEditStep(0); }, }, { @@ -67,7 +78,7 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { title: bankAccountPersonalDetails?.accountNumber, shouldShowRightIcon: true, onPress: () => { - onMove(0); + moveToEditStep(0); }, }, ] @@ -77,7 +88,7 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { title: selectedPlaidAccount?.addressName ?? '', shouldShowRightIcon: true, onPress: () => { - onMove(0); + moveToEditStep(0); }, }, ]; @@ -89,7 +100,7 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { title: `${personalDetails[PERSONAL_INFO_STEP_KEYS.FIRST_NAME]} ${personalDetails[PERSONAL_INFO_STEP_KEYS.LAST_NAME]}`, shouldShowRightIcon: true, onPress: () => { - onMove(1); + moveToEditStep(1); }, }, { @@ -97,7 +108,7 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { title: `${personalDetails?.addressStreet}, ${personalDetails?.addressCity}, ${personalDetails?.addressState} ${personalDetails?.addressZip}`, shouldShowRightIcon: true, onPress: () => { - onMove(2); + moveToEditStep(2); }, }, { @@ -105,7 +116,7 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { title: personalDetails[PERSONAL_INFO_STEP_KEYS.PHONE_NUMBER], shouldShowRightIcon: true, onPress: () => { - onMove(3); + moveToEditStep(3); }, }, ]; @@ -114,7 +125,7 @@ function ConfirmationStep({onNext, onMove, isEditing}: SubStepProps) { bankAccountPersonalDetails?.routingNumber, bankAccountPersonalDetails?.selectedPlaidAccountID, isManual, - onMove, + moveToEditStep, personalDetails, plaidData?.bankAccounts, translate, diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ManualBankAccountDetailsStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ManualBankAccountDetailsStep.tsx index 3b7522c8093f..8c9104e42659 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ManualBankAccountDetailsStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/ManualBankAccountDetailsStep.tsx @@ -21,7 +21,7 @@ type ManualProps = SubStepProps; const BANK_INFO_STEP_KEYS = INPUT_IDS.BANK_INFO_STEP; const STEP_FIELDS = [BANK_INFO_STEP_KEYS.ROUTING_NUMBER, BANK_INFO_STEP_KEYS.ACCOUNT_NUMBER]; -function ManualBankAccountDetailsStep({onNext}: ManualProps) { +function ManualBankAccountDetailsStep({onNext, isEditing}: ManualProps) { const [bankAccountPersonalDetails] = useOnyx(ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM_DRAFT, {canBeMissing: true}); const {translate} = useLocalize(); @@ -69,7 +69,7 @@ function ManualBankAccountDetailsStep({onNext}: ManualProps) { formID={ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_FORM} onSubmit={handleSubmit} validate={validate} - submitButtonText={translate('common.next')} + submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')} style={[styles.mh5, styles.flexGrow1]} > {translate('bankAccount.manuallyAdd')} diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts index f9fe809bb3b0..7c3507e42203 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/utils/getSkippedStepsPersonalInfo.ts @@ -11,7 +11,7 @@ function getSkippedStepsPersonalInfo(data?: Partial): nu skippedSteps.push(1); } - if (!!currentAddress?.addressLine1 && !!currentAddress?.city && currentAddress?.state && !!currentAddress?.zipCode) { + if (!!currentAddress?.street && !!currentAddress?.city && currentAddress?.state && !!currentAddress?.zip) { skippedSteps.push(2); } From fe7bb86eb7ba639fe9ce0d0edc9a71edb62a6647 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 26 Nov 2025 13:44:48 +0200 Subject: [PATCH 0026/1015] remove direct creating of objects --- .../PersonalInfo/substeps/AddressStep.tsx | 17 ++++++++++------- .../PersonalInfo/substeps/LegalNameStep.tsx | 13 ++++++++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx index c08d5496e0ec..1781d5aa6412 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/AddressStep.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import CommonAddressStep from '@components/SubStepForms/AddressStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -25,12 +25,15 @@ function AddressStep({onNext, onMove, isEditing}: SubStepProps) { const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); const currentAddress = getCurrentAddress(privatePersonalDetails); - const defaultValues = { - street: currentAddress?.street ?? '', - city: currentAddress?.city ?? '', - state: currentAddress?.state ?? '', - zipCode: currentAddress?.zip ?? '', - }; + const defaultValues = useMemo( + () => ({ + street: currentAddress?.street ?? '', + city: currentAddress?.city ?? '', + state: currentAddress?.state ?? '', + zipCode: currentAddress?.zip ?? '', + }), + [currentAddress?.city, currentAddress?.state, currentAddress?.street, currentAddress?.zip], + ); const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ fieldIds: STEP_FIELDS, diff --git a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx index d027f886f97c..67eedcd4ad20 100644 --- a/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx +++ b/src/pages/settings/Wallet/InternationalDepositAccount/PersonalInfo/substeps/LegalNameStep.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import FullNameStep from '@components/SubStepForms/FullNameStep'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -14,10 +14,13 @@ function LegalNameStep({onNext, onMove, isEditing}: SubStepProps) { const {translate} = useLocalize(); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); - const defaultValues = { - firstName: privatePersonalDetails?.[PERSONAL_INFO_STEP_KEY.FIRST_NAME] ?? '', - lastName: privatePersonalDetails?.[PERSONAL_INFO_STEP_KEY.LAST_NAME] ?? '', - }; + const defaultValues = useMemo( + () => ({ + firstName: privatePersonalDetails?.legalFirstName ?? '', + lastName: privatePersonalDetails?.legalLastName ?? '', + }), + [privatePersonalDetails?.legalFirstName, privatePersonalDetails?.legalLastName], + ); const handleSubmit = usePersonalBankAccountDetailsFormSubmit({ fieldIds: STEP_FIELDS, From f5bd6d7cb89627da35564ea5d0cc71bd5c6a0f71 Mon Sep 17 00:00:00 2001 From: lorretheboy Date: Thu, 27 Nov 2025 02:39:01 +0800 Subject: [PATCH 0027/1015] fix: view issue --- .../MoneyRequestViewReportFields.tsx | 4 +++- src/components/ReportActionItem/MoneyReportView.tsx | 4 +++- src/libs/ReportUtils.ts | 8 +++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 0b6ddad88b40..8cee588e0fdb 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -113,7 +113,9 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, }); }, [policy, report, violations]); - const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !isReportFieldDisabled(report, reportField, policy)); + const enabledReportFields = sortedPolicyReportFields.filter( + (reportField) => !isReportFieldDisabled(report, reportField, policy) || reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA, + ); const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 5309bd092134..67fb7fa10e5f 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -93,7 +93,9 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo return fields.filter((field) => field.target === report?.type).sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); }, [policy, report]); - const enabledReportFields = sortedPolicyReportFields.filter((reportField) => !isReportFieldDisabled(report, reportField, policy)); + const enabledReportFields = sortedPolicyReportFields.filter( + (reportField) => !isReportFieldDisabled(report, reportField, policy) || reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA, + ); const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); const isClosedExpenseReportWithNoExpenses = isClosedExpenseReportWithNoExpensesReportUtils(report); const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 43926fa7eb3a..a4345b1dd840 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4276,15 +4276,13 @@ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry const isTitleField = isReportFieldOfTypeTitle(reportField); const isAdmin = isPolicyAdmin(report?.policyID, {[`${ONYXKEYS.COLLECTION.POLICY}${policy?.id}`]: policy}); const isApproved = isReportApproved({report}); + const isFormulaField = reportField?.type === CONST.REPORT_FIELD_TYPES.FORMULA; + if (!isAdmin && (isReportSettled || isReportClosed || isApproved)) { return true; } - if (isTitleField) { - return !reportField?.deletable; - } - - if (reportField?.type === CONST.REPORT_FIELD_TYPES.FORMULA) { + if (isTitleField || isFormulaField) { return !reportField?.deletable; } From 27b9cdc7b019070b4192c57228f1503d14e85b81 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 28 Nov 2025 19:04:44 +0200 Subject: [PATCH 0028/1015] Unshare bank account --- assets/images/user-minus.svg | 5 + src/ONYXKEYS.ts | 4 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../Icon/chunks/expensify-icons.chunk.ts | 2 + src/languages/en.ts | 6 + .../parameters/UnshareBankAccountParams.ts | 6 + src/libs/API/parameters/index.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../RELATIONS/SETTINGS_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 4 + src/libs/Navigation/types.ts | 3 + src/libs/actions/BankAccounts.ts | 56 +++++ .../UnshareBankAccount/UnshareBankAccount.tsx | 201 ++++++++++++++++++ .../settings/Wallet/WalletPage/WalletPage.tsx | 22 +- src/types/onyx/UnshareBankAccount.ts | 18 ++ src/types/onyx/index.ts | 2 + 17 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 assets/images/user-minus.svg create mode 100644 src/libs/API/parameters/UnshareBankAccountParams.ts create mode 100644 src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx create mode 100644 src/types/onyx/UnshareBankAccount.ts diff --git a/assets/images/user-minus.svg b/assets/images/user-minus.svg new file mode 100644 index 000000000000..c0926d61019b --- /dev/null +++ b/assets/images/user-minus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ff5685b0cca2..22b9f94f2ba4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -122,6 +122,9 @@ const ONYXKEYS = { /* Contains meta data for the call to the API to get the joinable policies */ VALIDATE_USER_AND_GET_ACCESSIBLE_POLICIES: 'validateUserAndGetAccessiblePolicies', + /** Stores information about the unshare bank account */ + UNSHARE_BANK_ACCOUNT: 'unshareBankAccount', + /** Information about the current session (authToken, accountID, email, loading, error) */ SESSION: 'session', STASHED_SESSION: 'stashedSession', @@ -1194,6 +1197,7 @@ type OnyxValuesMapping = { [ONYXKEYS.WALLET_STATEMENT]: OnyxTypes.WalletStatement; [ONYXKEYS.PURCHASE_LIST]: OnyxTypes.PurchaseList; [ONYXKEYS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; + [ONYXKEYS.UNSHARE_BANK_ACCOUNT]: OnyxTypes.UnshareBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_OPTION_PRESSED]: ValueOf; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: string | number; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 3893661d7645..a97a0e7c3c7e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -322,6 +322,10 @@ const ROUTES = { SETTINGS_ADD_US_BANK_ACCOUNT: 'settings/wallet/add-us-bank-account', SETTINGS_ADD_BANK_ACCOUNT_SELECT_COUNTRY_VERIFY_ACCOUNT: `settings/wallet/add-bank-account/select-country/${VERIFY_ACCOUNT}`, SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', + SETTINGS_WALLET_UNSHARE_BANK_ACCOUNT: { + route: 'settings/wallet/:bankAccountID/unshare-bank-account', + getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/unshare-bank-account` as const, + }, SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS: { route: 'settings/wallet/:bankAccountID/enable-global-reimbursements', getRoute: (bankAccountID: number | undefined) => `settings/wallet/${bankAccountID}/enable-global-reimbursements` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9584b2a5f359..831d7b52a070 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -161,6 +161,7 @@ const SCREENS = { REPORT_VIRTUAL_CARD_FRAUD_CONFIRM_MAGIC_CODE: 'Settings_Wallet_ReportVirtualCardFraud_ConfirmMagicCode', REPORT_VIRTUAL_CARD_FRAUD_CONFIRMATION: 'Settings_Wallet_ReportVirtualCardFraudConfirmation', CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', + UNSHARE_BANK_ACCOUNT: 'Settings_Wallet_Unshare_Bank_Account', ENABLE_GLOBAL_REIMBURSEMENTS: 'Settings_Wallet_Enable_Global_Reimbursements', }, diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts index 61d793bcc2c2..a5461688fe9c 100644 --- a/src/components/Icon/chunks/expensify-icons.chunk.ts +++ b/src/components/Icon/chunks/expensify-icons.chunk.ts @@ -214,6 +214,7 @@ import Upload from '@assets/images/upload.svg'; import UserCheck from '@assets/images/user-check.svg'; import UserEye from '@assets/images/user-eye.svg'; import UserLock from '@assets/images/user-lock.svg'; +import UserMinus from '@assets/images/user-minus.svg'; import UserPlus from '@assets/images/user-plus.svg'; import User from '@assets/images/user.svg'; import Users from '@assets/images/users.svg'; @@ -330,6 +331,7 @@ const Expensicons = { LinkCopy, Location, Lock, + UserMinus, Luggage, MagnifyingGlass, Mail, diff --git a/src/languages/en.ts b/src/languages/en.ts index 74a57a3bd8d4..87c2b1402e38 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -330,6 +330,7 @@ const translations = { cancel: 'Cancel', dismiss: 'Dismiss', proceed: 'Proceed', + unshare: 'Unshare', yes: 'Yes', no: 'No', ok: 'OK', @@ -2142,6 +2143,11 @@ const translations = { confirmYourBankAccount: 'Confirm your bank account', personalBankAccounts: 'Personal bank accounts', businessBankAccounts: 'Business bank accounts', + unshareBankAccount: 'Unshare bank account', + unshareBankAccountDescription: 'Everyone below has access to this bank account. You can remove access at any point. We’ll still complete any payments in process.', + unshareBankAccountWarning: ({admin}: {admin?: string | null}) => `${admin} will lose access to this business bank account. We’ll still complete any payments in process.`, + reachOutForHelp: 'It’s being used with the Expensify Card. Reach out to Concierge if you need to unshare it.', + unshareErrorModalTitle: 'Can’t unshare bank account', }, cardPage: { expensifyCard: 'Expensify Card', diff --git a/src/libs/API/parameters/UnshareBankAccountParams.ts b/src/libs/API/parameters/UnshareBankAccountParams.ts new file mode 100644 index 000000000000..420f09fa32ca --- /dev/null +++ b/src/libs/API/parameters/UnshareBankAccountParams.ts @@ -0,0 +1,6 @@ +type UnshareBankAccountParams = { + bankAccountID: number; + email: string; +}; + +export default UnshareBankAccountParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 09553e22d5e0..62fd9c973db6 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -135,6 +135,7 @@ export type {default as TogglePolicyUberAutoRemovePageParams} from './TogglePoli export type {default as InviteToRoomParams} from './InviteToRoomParams'; export type {default as InviteToGroupChatParams} from './InviteToGroupChatParams'; export type {default as InviteWorkspaceEmployeesToUberParams} from './InviteWorkspaceEmployeesToUberParams'; +export type {default as UnshareBankAccountParams} from './UnshareBankAccountParams'; export type {default as RemoveFromRoomParams} from './RemoveFromRoomParams'; export type {default as RemoveFromGroupChatParams} from './RemoveFromGroupChatParams'; export type {default as FlagCommentParams} from './FlagCommentParams'; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 14698f195184..50a826f17bd7 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -442,6 +442,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Wallet/TransferBalancePage').default, [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../../pages/settings/Wallet/ChooseTransferAccountPage').default, [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: () => require('../../../../pages/EnablePayments/EnablePayments').default, + [SCREENS.SETTINGS.WALLET.UNSHARE_BANK_ACCOUNT]: () => require('../../../../pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount').default, [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: () => require('../../../../pages/settings/Wallet/EnableGlobalReimbursements').default, [SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../../pages/settings/Wallet/AddDebitCardPage').default, [SCREENS.SETTINGS.ADD_BANK_ACCOUNT_VERIFY_ACCOUNT]: () => require('../../../../pages/settings/Wallet/NewBankAccountVerifyAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 9ee3dd508f81..2990a6ce6342 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -38,6 +38,7 @@ const SETTINGS_TO_RHP: Partial['config'] = { path: ROUTES.SETTINGS_ENABLE_PAYMENTS, exact: true, }, + [SCREENS.SETTINGS.WALLET.UNSHARE_BANK_ACCOUNT]: { + path: ROUTES.SETTINGS_WALLET_UNSHARE_BANK_ACCOUNT.route, + exact: true, + }, [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: { path: ROUTES.SETTINGS_WALLET_ENABLE_GLOBAL_REIMBURSEMENTS.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b2ef587a0955..f5616bc5eb9e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -196,6 +196,9 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: undefined; [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: undefined; [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: undefined; + [SCREENS.SETTINGS.WALLET.UNSHARE_BANK_ACCOUNT]: { + bankAccountID: string; + }; [SCREENS.SETTINGS.WALLET.ENABLE_GLOBAL_REIMBURSEMENTS]: { bankAccountID: string; }; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 01d686632d1d..3175d866b479 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -14,6 +14,7 @@ import type { OpenReimbursementAccountPageParams, SaveCorpayOnboardingBeneficialOwnerParams, SendReminderForCorpaySignerInformationParams, + UnshareBankAccountParams, ValidateBankAccountWithTransactionsParams, VerifyIdentityForBankAccountParams, } from '@libs/API/parameters'; @@ -1222,6 +1223,58 @@ function fetchCorpayFields(bankCountry: string, bankCurrency?: string, isWithdra ); } +function clearUnshareBankAccount() { + Onyx.set(ONYXKEYS.UNSHARE_BANK_ACCOUNT, null); +} + +function clearUnshareBankAccountErrors() { + Onyx.merge(ONYXKEYS.UNSHARE_BANK_ACCOUNT, {errors: null}); +} + +function unshareBankAccount(bankAccountID: number, email: string) { + const parameters: UnshareBankAccountParams = { + bankAccountID, + email, + }; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.UNSHARE_BANK_ACCOUNT, + value: { + isLoading: true, + errors: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.UNSHARE_BANK_ACCOUNT, + value: { + isLoading: false, + errors: null, + admins: null, + shouldShowSuccess: true, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.UNSHARE_BANK_ACCOUNT, + value: { + isLoading: false, + errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + ], + }; + + API.write(WRITE_COMMANDS.UNSHARE_BANK_ACCOUNT, parameters, onyxData); +} + function createCorpayBankAccountForWalletFlow(data: InternationalBankAccountForm, classification: string, destinationCountry: string, preferredMethod: string) { const inputData = { ...data, @@ -1323,6 +1376,9 @@ export { createCorpayBankAccountForWalletFlow, getCorpayOnboardingFields, saveCorpayOnboardingCompanyDetails, + unshareBankAccount, + clearUnshareBankAccountErrors, + clearUnshareBankAccount, clearReimbursementAccountSaveCorpayOnboardingCompanyDetails, saveCorpayOnboardingBeneficialOwners, saveCorpayOnboardingDirectorInformation, diff --git a/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx b/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx new file mode 100644 index 000000000000..e62901f06d24 --- /dev/null +++ b/src/pages/settings/Wallet/UnshareBankAccount/UnshareBankAccount.tsx @@ -0,0 +1,201 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import ConfirmModal from '@components/ConfirmModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import RenderHTML from '@components/RenderHTML'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionListWithSections'; +import type {ListItem} from '@components/SelectionListWithSections/types'; +import UserListItem from '@components/SelectionListWithSections/UserListItem'; +import Text from '@components/Text'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; +import {formatMemberForList, getHeaderMessage, getSearchValueForPhoneOrEmail} from '@libs/OptionsListUtils'; +import type {MemberForList} from '@libs/OptionsListUtils'; +import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; +import tokenizedSearch from '@libs/tokenizedSearch'; +import Navigation from '@navigation/Navigation'; +import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import {clearUnshareBankAccount, clearUnshareBankAccountErrors, unshareBankAccount} from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ShareBankAccountProps = PlatformStackScreenProps; + +const DEFAULT_OBJECT = {}; +function UnshareBankAccount({route}: ShareBankAccountProps) { + const bankAccountID = route.params?.bankAccountID; + const styles = useThemeStyles(); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + + const {isOffline} = useNetwork(); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + + const [unsharedBankAccountData] = useOnyx(ONYXKEYS.UNSHARE_BANK_ACCOUNT, {canBeMissing: true}); + const isLoading = unsharedBankAccountData?.isLoading ?? false; + + const [unshareUser, setUnshareUser] = useState<{login?: string | null; text?: string | null} | undefined>(undefined); + const error = getLatestErrorMessage(unsharedBankAccountData ?? DEFAULT_OBJECT); + + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const {translate} = useLocalize(); + const admins = ['n.arefyev91@gmail.com', '5552@gmail.com', '5553@gmail.com'] ?? bankAccountList?.[bankAccountID]?.accountData?.sharees; + // const shouldShowTextInput = admins && admins?.length >= CONST.STANDARD_LIST_ITEM_LIMIT; + const shouldShowTextInput = admins && admins?.length >= 2; + const textInputLabel = shouldShowTextInput ? translate('common.search') : undefined; + + useEffect(() => { + return () => { + if (isLoading) { + return; + } + clearUnshareBankAccount(); + }; + }, [isLoading]); + + useEffect(() => { + if (isOffline) { + return; + } + // openBankAccountSharePage(); + }, [isOffline]); + + const handleUnshare = useCallback(() => { + if (!bankAccountID || !unshareUser?.login) { + return; + } + unshareBankAccount(Number(bankAccountID), unshareUser.login); + setUnshareUser(undefined); + }, [bankAccountID, unshareUser?.login]); + + const adminsList = useMemo(() => { + if (admins?.length === 0) { + return []; + } + + const adminsWithInfo = + admins?.map((admin) => { + const personalDetails = getPersonalDetailByEmail(admin); + return formatMemberForList({ + text: personalDetails?.displayName, + alternateText: personalDetails?.login, + keyForList: personalDetails?.login, + accountID: personalDetails?.accountID, + login: personalDetails?.login, + pendingAction: personalDetails?.pendingAction, + reportID: '', + }); + }) ?? []; + + let adminsToDisplay = [...adminsWithInfo]; + + // Apply search filter if there's a search term + if (debouncedSearchTerm) { + const searchValue = getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode).toLowerCase(); + adminsToDisplay = tokenizedSearch(adminsWithInfo, searchValue, (option) => [option.text ?? '', option.alternateText ?? '']); + } + + return adminsToDisplay; + }, [admins, countryCode, debouncedSearchTerm]); + + const hideUnshareErrorModal = useCallback(() => { + clearUnshareBankAccountErrors(); + }, []); + + const onSelectRow = useCallback((item: MemberForList) => { + setUnshareUser({login: item?.login, text: item?.text}); + }, []); + + const itemRightSideComponent = useCallback( + (item: ListItem) => { + return ( +