From c0ebcb7cc0a73c2974d059f9088bf21188f9035c Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Thu, 27 Jun 2024 12:05:42 +0200 Subject: [PATCH 01/54] Pick only params from route before passing them to screen --- src/ROUTES.ts | 2 +- .../BottomTabBar/index.tsx | 2 +- .../BottomTabBar/index.website.tsx | 2 +- src/libs/Navigation/linkingConfig/config.ts | 2 +- .../linkingConfig/getAdaptedStateFromPath.ts | 22 ++++++++++++++----- src/libs/Navigation/switchPolicyID.ts | 2 +- src/pages/Search/SearchFilters.tsx | 8 +++---- src/pages/Search/SearchPage.tsx | 2 +- src/pages/Search/SearchPageBottomTab.tsx | 2 +- .../Subscription/CardSection/CardSection.tsx | 2 +- 10 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5c8cfdcc8a68..bcef69e1c8be 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -35,7 +35,7 @@ const ROUTES = { ALL_SETTINGS: 'all-settings', - SEARCH: { + SEARCH_CENTRAL_PANE: { route: '/search/:query', getRoute: (searchQuery: SearchQuery, queryParams?: AuthScreensParamList['Search_Central_Pane']) => { const {sortBy, sortOrder} = queryParams ?? {}; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx index 472d2c7d6d29..f4b03df94af7 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.tsx @@ -101,7 +101,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps { - interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL))); + interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL))); }} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.search')} diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx index 9fe78273bdb0..555b58368328 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx @@ -102,7 +102,7 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps { - interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL))); + interceptAnonymousUser(() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL))); }} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.search')} diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c66472abb3b4..133d97a8c4f2 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -51,7 +51,7 @@ const config: LinkingOptions['config'] = { exact: true, }, [SCREENS.SETTINGS.WORKSPACES]: ROUTES.SETTINGS_WORKSPACES, - [SCREENS.SEARCH.CENTRAL_PANE]: ROUTES.SEARCH.route, + [SCREENS.SEARCH.CENTRAL_PANE]: ROUTES.SEARCH_CENTRAL_PANE.route, [SCREENS.SETTINGS.SAVE_THE_WORLD]: ROUTES.SETTINGS_SAVE_THE_WORLD, [SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: ROUTES.SETTINGS_SUBSCRIPTION, diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 17ea0e17d1b9..b867fa4cd4df 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -1,5 +1,6 @@ import type {NavigationState, PartialState, Route} from '@react-navigation/native'; import {findFocusedRoute, getStateFromPath} from '@react-navigation/native'; +import pick from 'lodash/pick'; import type {TupleToUnion} from 'type-fest'; import {isAnonymousUser} from '@libs/actions/Session'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; @@ -8,6 +9,7 @@ import isCentralPaneName from '@libs/NavigationUtils'; import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; +import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; import config from './config'; @@ -92,6 +94,14 @@ function createFullScreenNavigator(route?: NavigationPartialRoute | undefined { // Check for backTo param. One screen with different backTo value may need diferent screens visible under the overlay. @@ -125,18 +135,18 @@ function getMatchingRootRouteForRHPRoute(route: NavigationPartialRoute): Navigat // Check for CentralPaneNavigator for (const [centralPaneName, RHPNames] of Object.entries(CENTRAL_PANE_TO_RHP_MAPPING)) { if (RHPNames.includes(route.name)) { - const params = {...route.params}; - if (centralPaneName === SCREENS.SEARCH.CENTRAL_PANE) { - delete (params as Record)?.reportID; - } - return {name: centralPaneName as CentralPaneName, params}; + const paramsFromRoute = getParamsFromRoute(centralPaneName); + + return {name: centralPaneName as CentralPaneName, params: pick(route.params, paramsFromRoute)}; } } // Check for FullScreenNavigator for (const [fullScreenName, RHPNames] of Object.entries(FULL_SCREEN_TO_RHP_MAPPING)) { if (RHPNames.includes(route.name)) { - return createFullScreenNavigator({name: fullScreenName as FullScreenName, params: route.params}); + const paramsFromRoute = getParamsFromRoute(fullScreenName); + + return createFullScreenNavigator({name: fullScreenName as FullScreenName, params: pick(route.params, paramsFromRoute)}); } } } diff --git a/src/libs/Navigation/switchPolicyID.ts b/src/libs/Navigation/switchPolicyID.ts index 59461bfc3c8f..995001964905 100644 --- a/src/libs/Navigation/switchPolicyID.ts +++ b/src/libs/Navigation/switchPolicyID.ts @@ -84,7 +84,7 @@ export default function switchPolicyID(navigation: NavigationContainerRef>; const action: StackNavigationAction = getActionFromState(stateFromPath, linkingConfig.config); diff --git a/src/pages/Search/SearchFilters.tsx b/src/pages/Search/SearchFilters.tsx index bbb861364f00..c25fe02a076f 100644 --- a/src/pages/Search/SearchFilters.tsx +++ b/src/pages/Search/SearchFilters.tsx @@ -36,25 +36,25 @@ function SearchFilters({query}: SearchFiltersProps) { title: translate('common.expenses'), query: CONST.SEARCH.TAB.ALL, icon: Expensicons.Receipt, - route: ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL), + route: ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL), }, { title: translate('common.shared'), query: CONST.SEARCH.TAB.SHARED, icon: Expensicons.Send, - route: ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.SHARED), + route: ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.SHARED), }, { title: translate('common.drafts'), query: CONST.SEARCH.TAB.DRAFTS, icon: Expensicons.Pencil, - route: ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.DRAFTS), + route: ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.DRAFTS), }, { title: translate('common.finished'), query: CONST.SEARCH.TAB.FINISHED, icon: Expensicons.CheckCircle, - route: ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.FINISHED), + route: ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.FINISHED), }, ]; const activeItemIndex = filterItems.findIndex((item) => item.query === query); diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 7890e53f1b3c..46e353293fe2 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -35,7 +35,7 @@ function SearchPage({route}: SearchPageProps) { finished: {icon: Illustrations.CheckmarkCircle, title: translate('common.finished')}, }; - const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL)); + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL)); // On small screens this page is not displayed, the configuration is in the file: src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx // To avoid calling hooks in the Search component when this page isn't visible, we return null here. diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index eb662fd49046..153dd6b8ddea 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -46,7 +46,7 @@ function SearchPageBottomTab() { const isValidQuery = Object.values(CONST.SEARCH.TAB).includes(query); - const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL)); + const handleOnBackButtonPress = () => Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL)); return ( Navigation.navigate(ROUTES.SEARCH.getRoute(CONST.SEARCH.TAB.ALL))} + onPress={() => Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute(CONST.SEARCH.TAB.ALL))} hoverAndPressStyle={styles.hoveredComponentBG} /> )} From 2acd09098c6ccf390638f62a5cb772598514b82e Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Fri, 28 Jun 2024 13:05:46 +0200 Subject: [PATCH 02/54] Add normalized configs --- src/libs/Navigation/linkingConfig/config.ts | 28 +++- .../linkingConfig/createNormalizedConfigs.ts | 133 ++++++++++++++++++ .../linkingConfig/getAdaptedStateFromPath.ts | 10 +- 3 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 5eabb4d33ca4..582c1a2341a1 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1,9 +1,11 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import type {LinkingOptions} from '@react-navigation/native'; import type {RootStackParamList} from '@navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; +import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; +import type {RouteConfig} from './createNormalizedConfigs'; +import createNormalizedConfigs from './createNormalizedConfigs'; // Moved to a separate file to avoid cyclic dependencies. const config: LinkingOptions['config'] = { @@ -826,4 +828,28 @@ const config: LinkingOptions['config'] = { }, }; +const normalizedConfigs = Object.keys(config.screens) + .map((key) => + createNormalizedConfigs( + key, + config.screens, + [], + config.initialRouteName + ? [ + { + initialRouteName: config.initialRouteName, + parentScreens: [], + }, + ] + : [], + [], + ), + ) + .flat() + .reduce((acc, route) => { + acc[route.screen as Screen] = route; + return acc; + }, {} as Record); + +export {normalizedConfigs}; export default config; diff --git a/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts b/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts new file mode 100644 index 000000000000..d8e5c00b48e2 --- /dev/null +++ b/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts @@ -0,0 +1,133 @@ +/* eslint-disable @typescript-eslint/default-param-last */ + +/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + +/* eslint-disable no-param-reassign */ + +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/* eslint-disable @typescript-eslint/ban-types */ +// THOSE FUNCTIONS ARE COPIED FROM react-navigation/core IN ORDER TO AVOID PATCHING +// THAT'S THE REASON WHY ESLINT IS DISABLED +import type {PathConfigMap} from '@react-navigation/native'; + +type ParseConfig = Record any>; + +type RouteConfig = { + screen: string; + regex?: RegExp; + path: string; + pattern: string; + routeNames: string[]; + parse?: ParseConfig; +}; + +type InitialRouteConfig = { + initialRouteName: string; + parentScreens: string[]; +}; + +const joinPaths = (...paths: string[]): string => + ([] as string[]) + .concat(...paths.map((p) => p.split('/'))) + .filter(Boolean) + .join('/'); + +const createConfigItem = (screen: string, routeNames: string[], pattern: string, path: string, parse?: ParseConfig): RouteConfig => { + // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc. + pattern = pattern.split('/').filter(Boolean).join('/'); + + const regex = pattern + ? new RegExp( + `^(${pattern + .split('/') + .map((it) => { + if (it.startsWith(':')) { + return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`; + } + + return `${it === '*' ? '.*' : escape(it)}\\/`; + }) + .join('')})`, + ) + : undefined; + + return { + screen, + regex, + pattern, + path, + // The routeNames array is mutated, so copy it to keep the current state + routeNames: [...routeNames], + parse, + }; +}; + +const createNormalizedConfigs = ( + screen: string, + routeConfig: PathConfigMap, + routeNames: string[] = [], + initials: InitialRouteConfig[], + parentScreens: string[], + parentPattern?: string, +): RouteConfig[] => { + const configs: RouteConfig[] = []; + + routeNames.push(screen); + + parentScreens.push(screen); + + // @ts-expect-error: we can't strongly typecheck this for now + const config = routeConfig[screen]; + + if (typeof config === 'string') { + // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern + const pattern = parentPattern ? joinPaths(parentPattern, config) : config; + + configs.push(createConfigItem(screen, routeNames, pattern, config)); + } else if (typeof config === 'object') { + let pattern: string | undefined; + + // if an object is specified as the value (e.g. Foo: { ... }), + // it can have `path` property and + // it could have `screens` prop which has nested configs + if (typeof config.path === 'string') { + if (config.exact && config.path === undefined) { + throw new Error("A 'path' needs to be specified when specifying 'exact: true'. If you don't want this screen in the URL, specify it as empty string, e.g. `path: ''`."); + } + + pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || ''; + + configs.push(createConfigItem(screen, routeNames, pattern!, config.path, config.parse)); + } + + if (config.screens) { + // property `initialRouteName` without `screens` has no purpose + if (config.initialRouteName) { + initials.push({ + initialRouteName: config.initialRouteName, + parentScreens, + }); + } + + Object.keys(config.screens).forEach((nestedConfig) => { + const result = createNormalizedConfigs(nestedConfig, config.screens as PathConfigMap, routeNames, initials, [...parentScreens], pattern ?? parentPattern); + + configs.push(...result); + }); + } + } + + routeNames.pop(); + + return configs; +}; + +export type {RouteConfig}; +export default createNormalizedConfigs; diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index b867fa4cd4df..4a06fdcb784b 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -9,10 +9,10 @@ import isCentralPaneName from '@libs/NavigationUtils'; import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; -import ROUTES from '@src/ROUTES'; +import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; -import config from './config'; +import config, {normalizedConfigs} from './config'; import extractPolicyIDsFromState from './extractPolicyIDsFromState'; import FULL_SCREEN_TO_RHP_MAPPING from './FULL_SCREEN_TO_RHP_MAPPING'; import getMatchingBottomTabRouteForState from './getMatchingBottomTabRouteForState'; @@ -94,10 +94,10 @@ function createFullScreenNavigator(route?: NavigationPartialRoute Date: Mon, 1 Jul 2024 09:02:35 +0200 Subject: [PATCH 03/54] Fix prettier --- src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts b/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts index d8e5c00b48e2..a92d36d8eddb 100644 --- a/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts +++ b/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts @@ -14,7 +14,7 @@ /* eslint-disable @typescript-eslint/ban-types */ // THOSE FUNCTIONS ARE COPIED FROM react-navigation/core IN ORDER TO AVOID PATCHING -// THAT'S THE REASON WHY ESLINT IS DISABLED +// THAT'S THE REASON WHY ESLINT IS DISABLED import type {PathConfigMap} from '@react-navigation/native'; type ParseConfig = Record any>; From 7b126db0ae0a5d8e13d794cba1ad5f107370eb99 Mon Sep 17 00:00:00 2001 From: Mahesh Vagicherla Date: Tue, 2 Jul 2024 01:04:17 +0530 Subject: [PATCH 04/54] remove upload photo step if using default avatar --- src/components/AvatarWithImagePicker.tsx | 159 ++++++++++++----------- 1 file changed, 86 insertions(+), 73 deletions(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 15a004ba9b87..860e2ccf3636 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {StyleSheet, View} from 'react-native'; import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; @@ -302,61 +302,26 @@ function AvatarWithImagePicker({ }); }, [isMenuVisible, windowWidth]); + const onPressAvatar = useCallback( + (openPicker: OpenPicker) => { + if (isUsingDefaultAvatar) { + openPicker({ + onPicked: showAvatarCropModal, + }); + return; + } + if (disabled && enablePreview && onViewPhotoPress) { + onViewPhotoPress(); + return; + } + setIsMenuVisible((prev) => !prev); + }, + [disabled, enablePreview, isUsingDefaultAvatar, onViewPhotoPress, showAvatarCropModal], + ); + return ( - - - { - if (disabled && enablePreview && onViewPhotoPress) { - onViewPhotoPress(); - return; - } - setIsMenuVisible((prev) => !prev); - }} - accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} - accessibilityLabel={translate('avatarWithImagePicker.editImage')} - disabled={isAvatarCropModalOpen || (disabled && !enablePreview)} - disabledStyle={disabledStyle} - style={[styles.pRelative, avatarStyle, type === CONST.ICON_TYPE_AVATAR && styles.alignSelfCenter]} - ref={anchorRef} - > - - {source ? ( - - ) : ( - - )} - - {!disabled && ( - - - - )} - - - setIsMenuVisible(false)} - onItemSelected={(item, index) => { - setIsMenuVisible(false); - // In order for the file picker to open dynamically, the click - // function must be called from within an event handler that was initiated - // by the user on Safari. - if (index === 0 && Browser.isSafari()) { - openPicker({ - onPicked: showAvatarCropModal, - }); - } - }} - menuItems={menuItems} - anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverMenuOffset(windowWidth) : popoverPosition} - anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} - withoutOverlay - anchorRef={anchorRef} - /> + <> + + + onPressAvatar(openPicker)} + accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} + accessibilityLabel={translate('avatarWithImagePicker.editImage')} + disabled={isAvatarCropModalOpen || (disabled && !enablePreview)} + disabledStyle={disabledStyle} + style={[styles.pRelative, avatarStyle, type === CONST.ICON_TYPE_AVATAR && styles.alignSelfCenter]} + ref={anchorRef} + > + + {source ? ( + + ) : ( + + )} + + {!disabled && ( + + + + )} + + + + setIsMenuVisible(false)} + onItemSelected={(item, index) => { + setIsMenuVisible(false); + // In order for the file picker to open dynamically, the click + // function must be called from within an event handler that was initiated + // by the user on Safari. + if (index === 0 && Browser.isSafari()) { + openPicker({ + onPicked: showAvatarCropModal, + }); + } + }} + menuItems={menuItems} + anchorPosition={shouldUseStyleUtilityForAnchorPosition ? styles.popoverMenuOffset(windowWidth) : popoverPosition} + anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}} + withoutOverlay + anchorRef={anchorRef} + /> + ); }} From becbfffcc1fd52a10ab091169f8a9c858d671dd6 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 10 Jul 2024 11:04:14 +0200 Subject: [PATCH 05/54] Fix blinking profile page --- src/libs/Navigation/getTopmostFullScreenRoute.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libs/Navigation/getTopmostFullScreenRoute.ts b/src/libs/Navigation/getTopmostFullScreenRoute.ts index 25c74ea0ce6b..fcc28ce76926 100644 --- a/src/libs/Navigation/getTopmostFullScreenRoute.ts +++ b/src/libs/Navigation/getTopmostFullScreenRoute.ts @@ -13,18 +13,16 @@ function getTopmostFullScreenRoute(state: State): Navigation return; } - if (!!topmostFullScreenRoute.params && 'screen' in topmostFullScreenRoute.params) { - return {name: topmostFullScreenRoute.params.screen as FullScreenName, params: topmostFullScreenRoute.params.params}; - } + if (topmostFullScreenRoute.state) { + // There will be at least one route in the fullscreen navigator. + const {name, params} = topmostFullScreenRoute.state.routes.at(-1) as NavigationPartialRoute; - if (!topmostFullScreenRoute.state) { - return; + return {name, params}; } - // There will be at least one route in the fullscreen navigator. - const {name, params} = topmostFullScreenRoute.state.routes.at(-1) as NavigationPartialRoute; - - return {name, params}; + if (!!topmostFullScreenRoute.params && 'screen' in topmostFullScreenRoute.params) { + return {name: topmostFullScreenRoute.params.screen as FullScreenName, params: topmostFullScreenRoute.params.params}; + } } export default getTopmostFullScreenRoute; From d3b58bc64adc79346506cadc1edec107b9d23138 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 10 Jul 2024 11:56:54 +0200 Subject: [PATCH 06/54] fix linter --- src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts b/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts index a92d36d8eddb..388c1499fbc5 100644 --- a/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts +++ b/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/default-param-last */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ From 30f11066a5f1923fc1134f927ebf9dae0236603a Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Wed, 10 Jul 2024 12:11:43 +0200 Subject: [PATCH 07/54] fix prettier --- src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts | 1 + src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts b/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts index 388c1499fbc5..9e21c7d073cb 100644 --- a/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts +++ b/src/libs/Navigation/linkingConfig/createNormalizedConfigs.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ + /* eslint-disable @typescript-eslint/default-param-last */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 294737e897cf..b6822be4ffaa 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -10,8 +10,8 @@ import {extractPolicyIDFromPath, getPathWithoutPolicyID} from '@libs/PolicyUtils import * as ReportConnection from '@libs/ReportConnection'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; -import type {Screen} from '@src/SCREENS'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; import CENTRAL_PANE_TO_RHP_MAPPING from './CENTRAL_PANE_TO_RHP_MAPPING'; import config, {normalizedConfigs} from './config'; From 3dfdf4ab29722b96f028f2aae3853d5d9c96fbc7 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Wed, 10 Jul 2024 18:14:54 +0200 Subject: [PATCH 08/54] fix initial value with empty values for list, add subtitle --- .../ReportFieldsInitialValuePage.tsx | 8 +++++++ .../reportFields/ReportFieldsSettingsPage.tsx | 21 +++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pages/workspace/reportFields/ReportFieldsInitialValuePage.tsx b/src/pages/workspace/reportFields/ReportFieldsInitialValuePage.tsx index 261d5a50f316..5e8fa86903dc 100644 --- a/src/pages/workspace/reportFields/ReportFieldsInitialValuePage.tsx +++ b/src/pages/workspace/reportFields/ReportFieldsInitialValuePage.tsx @@ -1,10 +1,12 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; @@ -105,6 +107,12 @@ function ReportFieldsInitialValuePage({ title={reportField.name} onBackButtonPress={Navigation.goBack} /> + {isListFieldType && ( + + {translate('workspace.reportFields.listValuesInputSubtitle')} + + )} + {isTextFieldType && ( { ReportField.deleteReportFields(policyID, [reportFieldKey]); @@ -80,15 +81,17 @@ function ReportFieldsSettingsPage({ description={translate('common.type')} interactive={false} /> - Navigation.navigate(ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.getRoute(policyID, reportFieldID))} - /> + {!isListFieldEmpty && ( + Navigation.navigate(ROUTES.WORKSPACE_EDIT_REPORT_FIELDS_INITIAL_VALUE.getRoute(policyID, reportFieldID))} + /> + )} {isListFieldType && ( Date: Wed, 10 Jul 2024 18:24:36 +0200 Subject: [PATCH 09/54] fix delete all in offline mode --- .../workspace/reportFields/WorkspaceReportFieldsPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx index 50b3f8394584..25effc8660a1 100644 --- a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx +++ b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx @@ -19,6 +19,7 @@ import type {ListItem} from '@components/SelectionList/types'; import Text from '@components/Text'; import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -65,6 +66,7 @@ function WorkspaceReportFieldsPage({ }, [policy]); const [selectedReportFields, setSelectedReportFields] = useState([]); const [deleteReportFieldsConfirmModalVisible, setDeleteReportFieldsConfirmModalVisible] = useState(false); + const {isOffline} = useNetwork(); useEffect(() => { if (isFocused) { @@ -126,9 +128,9 @@ function WorkspaceReportFieldsPage({ setDeleteReportFieldsConfirmModalVisible(false); }; - const isLoading = policy === undefined; + const isLoading = !isOffline && policy === undefined; const shouldShowEmptyState = - Object.values(filteredPolicyFieldList).filter((reportField) => reportField.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length <= 0 && !isLoading; + !Object.values(filteredPolicyFieldList).some((reportField) => reportField.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline) && !isLoading; const getHeaderButtons = () => { const options: Array>> = []; From 53fdbdb2c982a5458dd13d709377c37c8c11f4e0 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Wed, 10 Jul 2024 18:37:18 +0200 Subject: [PATCH 10/54] added openPolicyReportFieldsPage API call --- .../OpenPolicyReportFieldsPageParams.ts | 5 +++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/actions/Policy/ReportField.ts | 18 +++++++++++++++++- .../reportFields/WorkspaceReportFieldsPage.tsx | 13 ++++++++++--- 5 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 src/libs/API/parameters/OpenPolicyReportFieldsPageParams.ts diff --git a/src/libs/API/parameters/OpenPolicyReportFieldsPageParams.ts b/src/libs/API/parameters/OpenPolicyReportFieldsPageParams.ts new file mode 100644 index 000000000000..80d5d1f1aa07 --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyReportFieldsPageParams.ts @@ -0,0 +1,5 @@ +type OpenPolicyReportFieldsPageParams = { + policyID: string; +}; + +export default OpenPolicyReportFieldsPageParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 962056d9484a..adfbf3e6a3df 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -195,6 +195,7 @@ export type {default as CreatePolicyTaxParams} from './CreatePolicyTaxParams'; export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflowsPageParams'; export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; export type {default as OpenPolicyTaxesPageParams} from './OpenPolicyTaxesPageParams'; +export type {default as OpenPolicyReportFieldsPageParams} from './OpenPolicyReportFieldsPageParams'; export type {default as EnablePolicyTaxesParams} from './EnablePolicyTaxesParams'; export type {default as OpenPolicyMoreFeaturesPageParams} from './OpenPolicyMoreFeaturesPageParams'; export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDistanceRateParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 92a876b7b52f..f935553d6218 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -657,6 +657,7 @@ const READ_COMMANDS = { OPEN_POLICY_CATEGORIES_PAGE: 'OpenPolicyCategoriesPage', OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage', OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage', + OPEN_POLICY_REPORT_FIELDS_PAGE: 'OpenPolicyReportFieldsPage', OPEN_POLICY_EXPENSIFY_CARDS_PAGE: 'OpenPolicyExpensifyCardsPage', OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage', OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest', @@ -709,6 +710,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_CATEGORIES_PAGE]: Parameters.OpenPolicyCategoriesPageParams; [READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams; [READ_COMMANDS.OPEN_POLICY_TAXES_PAGE]: Parameters.OpenPolicyTaxesPageParams; + [READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE]: Parameters.OpenPolicyReportFieldsPageParams; [READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams; [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; [READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams; diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index 27b67c9fe686..3538a348bcca 100644 --- a/src/libs/actions/Policy/ReportField.ts +++ b/src/libs/actions/Policy/ReportField.ts @@ -7,11 +7,13 @@ import type { CreateWorkspaceReportFieldParams, DeletePolicyReportField, EnableWorkspaceReportFieldListValueParams, + OpenPolicyReportFieldsPageParams, RemoveWorkspaceReportFieldListValueParams, UpdateWorkspaceReportFieldInitialValueParams, } from '@libs/API/parameters'; -import {WRITE_COMMANDS} from '@libs/API/types'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Log from '@libs/Log'; import * as ReportUtils from '@libs/ReportUtils'; import * as WorkspaceReportFieldUtils from '@libs/WorkspaceReportFieldUtils'; import CONST from '@src/CONST'; @@ -69,6 +71,19 @@ Onyx.connect({ }, }); +function openPolicyReportFieldsPage(policyID: string) { + if (!policyID) { + Log.warn('openPolicyReportFieldsPage invalid params', {policyID}); + return; + } + + const params: OpenPolicyReportFieldsPageParams = { + policyID, + }; + + API.read(READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE, params); +} + /** * Sets the initial form values for the workspace report fields form. */ @@ -540,6 +555,7 @@ export { deleteReportFields, updateReportFieldInitialValue, updateReportFieldListValueEnabled, + openPolicyReportFieldsPage, addReportFieldListValue, removeReportFieldListValue, }; diff --git a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx index 25effc8660a1..94a5a3d356af 100644 --- a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx +++ b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx @@ -1,7 +1,7 @@ -import {useIsFocused} from '@react-navigation/native'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import {Str} from 'expensify-common'; -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -66,7 +66,14 @@ function WorkspaceReportFieldsPage({ }, [policy]); const [selectedReportFields, setSelectedReportFields] = useState([]); const [deleteReportFieldsConfirmModalVisible, setDeleteReportFieldsConfirmModalVisible] = useState(false); - const {isOffline} = useNetwork(); + + const fetchReportFields = useCallback(() => { + ReportField.openPolicyReportFieldsPage(policyID); + }, [policyID]); + + const {isOffline} = useNetwork({onReconnect: fetchReportFields}); + + useFocusEffect(fetchReportFields); useEffect(() => { if (isFocused) { From b7f03f622b68667d13a49e82c5bcbbdd6aca4a2b Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 11 Jul 2024 15:59:13 +0200 Subject: [PATCH 11/54] Implement Edit card limit page and route --- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 2 +- src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 4 + .../WorkspaceEditCardLimitPage.tsx | 97 +++++++++++++++++++ src/types/form/EditExpensifyCardLimit.ts | 13 +++ src/types/form/index.ts | 1 + src/types/onyx/Card.ts | 3 + 13 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx create mode 100644 src/types/form/EditExpensifyCardLimit.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index bd4b294a6d68..19b8f4113e9b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -561,6 +561,8 @@ const ONYXKEYS = { SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', + EDIT_EXPENSIFY_CARD_LIMIT: 'editExpensifyCardLimit', + EDIT_EXPENSIFY_CARD_LIMIT_DRAFT: 'editExpensifyCardLimitDraft', SAGE_INTACCT_CREDENTIALS_FORM: 'sageIntacctCredentialsForm', SAGE_INTACCT_CREDENTIALS_FORM_DRAFT: 'sageIntacctCredentialsFormDraft', NETSUITE_CUSTOM_FIELD_FORM: 'netSuiteCustomFieldForm', @@ -637,6 +639,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; + [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT]: FormTypes.EditExpensifyCardLimit; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a54bb4f5cca5..e59530a80800 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -826,6 +826,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/expensify-card', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const, }, + WORKSPACE_EXPENSIFY_CARD_LIMIT: { + route: 'settings/workspaces/:policyID/expensify-card/:cardID/limit', + getRoute: (policyID: string, cardID: string) => `settings/workspaces/${policyID}/expensify-card/${cardID}/limit` as const, + }, WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { route: 'settings/workspaces/:policyID/expensify-card/issue-new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index d2a6b7c19ddd..94c950196475 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -339,6 +339,7 @@ const SCREENS = { RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate', RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit', EXPENSIFY_CARD: 'Workspace_ExpensifyCard', + EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit', EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New', BILLS: 'Workspace_Bills', INVOICES: 'Workspace_Invoices', diff --git a/src/languages/en.ts b/src/languages/en.ts index 1ac9684ac22e..6a62fd62d1f6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2631,6 +2631,7 @@ export default { limit: 'Limit', currentBalance: 'Current balance', currentBalanceDescription: 'Current balance is the sum of all posted Expensify Card transactions that have occurred since the last settlement date.', + cardLimit: 'Card limit', remainingLimit: 'Remaining limit', requestLimitIncrease: 'Request limit increase', remainingLimitDescription: diff --git a/src/languages/es.ts b/src/languages/es.ts index ea9186daee78..b37a79b1bf00 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2677,6 +2677,7 @@ export default { currentBalance: 'Saldo actual', currentBalanceDescription: 'El saldo actual es la suma de todas las transacciones contabilizadas con la Tarjeta Expensify que se han producido desde la última fecha de liquidación.', + cardLimit: 'Límite de la tarjeta', remainingLimit: 'Límite restante', requestLimitIncrease: 'Solicitar aumento de límite', remainingLimitDescription: diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0b94972b2aa9..343dcc549ce8 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -403,6 +403,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/ValuePage').default, [SCREENS.WORKSPACE.TAX_CREATE]: () => require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default, + [SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceEditCardLimitPage').default, [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default, [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default, [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: () => require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 54804a495754..6e0dbe968560 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -150,7 +150,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE, SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE, ], - [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW], + [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c0d1a79f635f..214b80e8a8a8 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -459,6 +459,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.SHARE]: { path: ROUTES.WORKSPACE_PROFILE_SHARE.route, }, + [SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT]: { + path: ROUTES.WORKSPACE_EXPENSIFY_CARD_LIMIT.route, + }, [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: { path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fc67fe6b8cc0..619be2b36051 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -623,6 +623,10 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: { policyID: string; }; + [SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT]: { + policyID: string; + cardID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx new file mode 100644 index 000000000000..97d7eb462501 --- /dev/null +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -0,0 +1,97 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import AmountForm from '@components/AmountForm'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import Navigation from '@navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/EditExpensifyCardLimit'; + +// TODO: remove when Onyx data is available +const mockedCard = { + accountID: 885646, + availableSpend: 1000, + nameValuePairs: { + cardTitle: 'Test 1', + isVirtual: true, + limit: 2000, + limitType: CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART, + }, + lastFourPAN: '1234', +}; + +type WorkspaceEditCardLimitPageProps = StackScreenProps; + +function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { + const {policyID, cardID} = route.params; + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const styles = useThemeStyles(); + + const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`); + const card = cardsList?.[cardID] ?? mockedCard; + + const submit = (values: FormOnyxValues) => {}; + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.LIMIT]); + + // We only want integers to be sent as the limit + if (!Number(values.limit) || !Number.isInteger(Number(values.limit))) { + errors.limit = translate('iou.error.invalidAmount'); + } + + return errors; + }, + [translate], + ); + + return ( + + Navigation.goBack()} + /> + + + + + ); +} + +WorkspaceEditCardLimitPage.displayName = 'WorkspaceEditCardLimitPage'; + +export default WorkspaceEditCardLimitPage; diff --git a/src/types/form/EditExpensifyCardLimit.ts b/src/types/form/EditExpensifyCardLimit.ts new file mode 100644 index 000000000000..b651a617c20a --- /dev/null +++ b/src/types/form/EditExpensifyCardLimit.ts @@ -0,0 +1,13 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + LIMIT: 'limit', +} as const; + +type InputID = ValueOf; + +type EditExpensifyCardLimit = Form; + +export type {EditExpensifyCardLimit}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index dbfc6e5095f6..b76c1d4fdab4 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -60,4 +60,5 @@ export type {SageIntactCredentialsForm} from './SageIntactCredentialsForm'; export type {NetSuiteCustomFieldForm} from './NetSuiteCustomFieldForm'; export type {NetSuiteTokenInputForm} from './NetSuiteTokenInputForm'; export type {NetSuiteCustomFormIDForm} from './NetSuiteCustomFormIDForm'; +export type {EditExpensifyCardLimit} from './EditExpensifyCardLimit'; export type {default as Form} from './Form'; diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index cdaaddd7dce2..eb584fa73ab0 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -39,6 +39,9 @@ type Card = { /** Type of card spending limits */ limitType?: ValueOf; + /** Card spending limit */ + limit?: number; + /** User-defined nickname for the card */ cardTitle?: string; From 2d791ac065ae2ce4afb190c94c7ea3da1bf9bb5f Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 12 Jul 2024 10:59:18 +0200 Subject: [PATCH 12/54] Revert "Revert "Fix show onboarding modal functions"" This reverts commit a094775de7eafe1e7278628e46cf81c891bd518c. --- src/CONST.ts | 4 ++ src/ONYXKEYS.ts | 4 ++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../Navigators/OnboardingModalNavigator.tsx | 12 ++-- .../BottomTabBar/index.website.tsx | 11 ++- .../CustomRouter.ts | 32 ++++++++- src/libs/Navigation/NavigationRoot.tsx | 46 +++++++++---- src/libs/Navigation/types.ts | 3 + src/libs/NavigationUtils.ts | 14 +++- src/libs/actions/Report.ts | 68 ++++++++++++------- src/libs/actions/Welcome.ts | 39 ++++------- .../hasCompletedGuidedSetupFlowSelector.ts | 12 ++++ .../BaseOnboardingPersonalDetails.tsx | 7 +- .../BaseOnboardingPurpose.tsx | 39 +++++------ src/pages/OnboardingPurpose/types.ts | 23 ++----- .../OnboardingWork/BaseOnboardingWork.tsx | 3 - tests/ui/UnreadIndicatorsTest.tsx | 4 ++ 18 files changed, 202 insertions(+), 121 deletions(-) create mode 100644 src/libs/hasCompletedGuidedSetupFlowSelector.ts diff --git a/src/CONST.ts b/src/CONST.ts index c1ceb03f8be1..2f34b640c815 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5273,6 +5273,10 @@ const CONST = { DATE: 'date', LIST: 'dropdown', }, + + NAVIGATION_ACTIONS: { + RESET: 'RESET', + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b06b05dac7e1..8abb7738289c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -320,6 +320,9 @@ const ONYXKEYS = { /** Onboarding Purpose selected by the user during Onboarding flow */ ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected', + /** Onboarding error message to be displayed to the user */ + ONBOARDING_ERROR_MESSAGE: 'onboardingErrorMessage', + /** Onboarding policyID selected by the user during Onboarding flow */ ONBOARDING_POLICY_ID: 'onboardingPolicyID', @@ -798,6 +801,7 @@ type OnyxValuesMapping = { [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string; + [ONYXKEYS.ONBOARDING_ERROR_MESSAGE]: string; [ONYXKEYS.ONBOARDING_POLICY_ID]: string; [ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID]: string; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean; diff --git a/src/languages/en.ts b/src/languages/en.ts index 3307862c0fe7..27940a53487e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1469,6 +1469,7 @@ export default { title: 'What do you want to do today?', errorSelection: 'Please make a selection to continue.', errorContinue: 'Please press continue to get set up.', + errorBackButton: 'Please finish the setup questions to start using the app.', [CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Get paid back by my employer', [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: "Manage my team's expenses", [CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Track and budget expenses', diff --git a/src/languages/es.ts b/src/languages/es.ts index e757f10afb11..ab5ebc719a27 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1479,6 +1479,7 @@ export default { title: '¿Qué quieres hacer hoy?', errorSelection: 'Por favor selecciona una opción para continuar.', errorContinue: 'Por favor, haz click en continuar para configurar tu cuenta.', + errorBackButton: 'Por favor, finaliza las preguntas de configuración para empezar a utilizar la aplicación.', [CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Cobrar de mi empresa', [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo', [CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Controlar y presupuestar gastos', diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 29a2205b2e37..61adcd77da76 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -4,9 +4,11 @@ import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import NoDropZone from '@components/DragAndDrop/NoDropZone'; import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; +import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector'; import OnboardingModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/OnboardingModalNavigatorScreenOptions'; import Navigation from '@libs/Navigation/Navigation'; import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; @@ -26,15 +28,11 @@ function OnboardingModalNavigator() { const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useOnboardingLayout(); const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { - selector: (onboarding) => { - // onboarding is an array for old accounts and accounts created from olddot - if (Array.isArray(onboarding)) { - return true; - } - return onboarding?.hasCompletedGuidedSetupFlow; - }, + selector: hasCompletedGuidedSetupFlowSelector, }); + useDisableModalDismissOnEscape(); + useEffect(() => { if (!hasCompletedGuidedSetupFlow) { return; diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx index 8c531a918af8..2e1c4c012156 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar/index.website.tsx @@ -15,7 +15,9 @@ import * as Session from '@libs/actions/Session'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; -import Navigation from '@libs/Navigation/Navigation'; +import linkingConfig from '@libs/Navigation/linkingConfig'; +import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import type {RootStackParamList, State} from '@libs/Navigation/types'; import {isCentralPaneName} from '@libs/NavigationUtils'; import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils'; @@ -53,7 +55,12 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps return; } - Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_ROOT)}); + Welcome.isOnboardingFlowCompleted({ + onNotCompleted: () => { + const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config); + navigationRef.resetRoot(adaptedState); + }, + }); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isLoadingApp]); diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts index a1768df5e0d6..5b3cefb63a2d 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts @@ -1,13 +1,16 @@ -import type {RouterConfigOptions, StackNavigationState} from '@react-navigation/native'; -import {getPathFromState, StackRouter} from '@react-navigation/native'; +import type {CommonActions, RouterConfigOptions, StackActionType, StackNavigationState} from '@react-navigation/native'; +import {findFocusedRoute, getPathFromState, StackRouter} from '@react-navigation/native'; import type {ParamListBase} from '@react-navigation/routers'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import * as Localize from '@libs/Localize'; import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute'; import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute'; import linkingConfig from '@libs/Navigation/linkingConfig'; import getAdaptedStateFromPath from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath'; import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types'; -import {isCentralPaneName} from '@libs/NavigationUtils'; +import {isCentralPaneName, isOnboardingFlowName} from '@libs/NavigationUtils'; +import * as Welcome from '@userActions/Welcome'; +import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import type {ResponsiveStackNavigatorRouterOptions} from './types'; @@ -97,6 +100,23 @@ function compareAndAdaptState(state: StackNavigationState) { } } +function shouldPreventReset(state: StackNavigationState, action: CommonActions.Action | StackActionType) { + if (action.type !== CONST.NAVIGATION_ACTIONS.RESET || !action?.payload) { + return false; + } + const currentFocusedRoute = findFocusedRoute(state); + const targetFocusedRoute = findFocusedRoute(action?.payload); + + // We want to prevent the user from navigating back to a non-onboarding screen if they are currently on an onboarding screen + if (isOnboardingFlowName(currentFocusedRoute?.name) && !isOnboardingFlowName(targetFocusedRoute?.name)) { + Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton')); + // We reset the URL as the browser sets it in a way that doesn't match the navigation state + // eslint-disable-next-line no-restricted-globals + history.replaceState({}, '', getPathFromState(state, linkingConfig.config)); + return true; + } +} + function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) { const stackRouter = StackRouter(options); @@ -107,6 +127,12 @@ function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) { const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList}); return state; }, + getStateForAction(state: StackNavigationState, action: CommonActions.Action | StackActionType, configOptions: RouterConfigOptions) { + if (shouldPreventReset(state, action)) { + return state; + } + return stackRouter.getStateForAction(state, action, configOptions); + }, }; } diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 63792be4f79f..a225831b56ff 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -1,6 +1,7 @@ import type {NavigationState} from '@react-navigation/native'; import {DefaultTheme, findFocusedRoute, NavigationContainer} from '@react-navigation/native'; import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import {useOnyx} from 'react-native-onyx'; import HybridAppMiddleware from '@components/HybridAppMiddleware'; import {ScrollOffsetContext} from '@components/ScrollOffsetContextProvider'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; @@ -8,11 +9,14 @@ import useCurrentReportID from '@hooks/useCurrentReportID'; import useTheme from '@hooks/useTheme'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {FSPage} from '@libs/Fullstory'; +import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector'; import Log from '@libs/Log'; import {getPathFromURL} from '@libs/Url'; import {updateLastVisitedPath} from '@userActions/App'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; import AppNavigator from './AppNavigator'; import getPolicyIDFromState from './getPolicyIDFromState'; import linkingConfig from './linkingConfig'; @@ -77,25 +81,37 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N const {isSmallScreenWidth} = useWindowDimensions(); const {setActiveWorkspaceID} = useActiveWorkspace(); - const initialState = useMemo( - () => { - if (!lastVisitedPath) { - return undefined; - } + const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { + selector: hasCompletedGuidedSetupFlowSelector, + }); - const path = initialUrl ? getPathFromURL(initialUrl) : null; + const initialState = useMemo(() => { + // If the user haven't completed the flow, we want to always redirect them to the onboarding flow. + if (!hasCompletedGuidedSetupFlow) { + const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config); + return adaptedState; + } - // For non-nullable paths we don't want to set initial state - if (path) { - return; - } + // If there is no lastVisitedPath, we can do early return. We won't modify the default behavior. + if (!lastVisitedPath) { + return undefined; + } - const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config); - return adaptedState; - }, + const path = initialUrl ? getPathFromURL(initialUrl) : null; + + // If the user opens the root of app "/" it will be parsed to empty string "". + // If the path is defined and different that empty string we don't want to modify the default behavior. + if (path) { + return; + } + + // Otherwise we want to redirect the user to the last visited path. + const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config); + return adaptedState; + + // The initialState value is relevant only on the first render. // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - [], - ); + }, []); // https://reactnavigation.org/docs/themes const navigationTheme = useMemo( diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9ae469fd8bcc..00fd98dc51aa 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1212,6 +1212,8 @@ type FullScreenName = keyof FullScreenNavigatorParamList; type CentralPaneName = keyof CentralPaneScreensParamList; +type OnboardingFlowName = keyof OnboardingModalNavigatorParamList; + type SwitchPolicyIDParams = { policyID?: string; route?: Routes; @@ -1242,6 +1244,7 @@ export type { NewChatNavigatorParamList, NewTaskNavigatorParamList, OnboardingModalNavigatorParamList, + OnboardingFlowName, ParticipantsNavigatorParamList, PrivateNotesNavigatorParamList, ProfileNavigatorParamList, diff --git a/src/libs/NavigationUtils.ts b/src/libs/NavigationUtils.ts index 34fc0b971ef6..aa26268977a2 100644 --- a/src/libs/NavigationUtils.ts +++ b/src/libs/NavigationUtils.ts @@ -1,7 +1,7 @@ import cloneDeep from 'lodash/cloneDeep'; import SCREENS from '@src/SCREENS'; import getTopmostBottomTabRoute from './Navigation/getTopmostBottomTabRoute'; -import type {CentralPaneName, RootStackParamList, State} from './Navigation/types'; +import type {CentralPaneName, OnboardingFlowName, RootStackParamList, State} from './Navigation/types'; const CENTRAL_PANE_SCREEN_NAMES = new Set([ SCREENS.SETTINGS.WORKSPACES, @@ -17,6 +17,8 @@ const CENTRAL_PANE_SCREEN_NAMES = new Set([ SCREENS.REPORT, ]); +const ONBOARDING_SCREEN_NAMES = new Set([SCREENS.ONBOARDING.PERSONAL_DETAILS, SCREENS.ONBOARDING.PURPOSE, SCREENS.ONBOARDING.WORK, SCREENS.ONBOARDING_MODAL.ONBOARDING]); + function isCentralPaneName(screen: string | undefined): screen is CentralPaneName { if (!screen) { return false; @@ -25,6 +27,14 @@ function isCentralPaneName(screen: string | undefined): screen is CentralPaneNam return CENTRAL_PANE_SCREEN_NAMES.has(screen as CentralPaneName); } +function isOnboardingFlowName(screen: string | undefined): screen is OnboardingFlowName { + if (!screen) { + return false; + } + + return ONBOARDING_SCREEN_NAMES.has(screen as OnboardingFlowName); +} + const removePolicyIDParamFromState = (state: State) => { const stateCopy = cloneDeep(state); const bottomTabRoute = getTopmostBottomTabRoute(stateCopy); @@ -34,4 +44,4 @@ const removePolicyIDParamFromState = (state: State) => { return stateCopy; }; -export {isCentralPaneName, removePolicyIDParamFromState}; +export {isCentralPaneName, removePolicyIDParamFromState, isOnboardingFlowName}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 026ce45146d3..3060f53f12c3 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1,3 +1,4 @@ +import {findFocusedRoute} from '@react-navigation/native'; import {format as timezoneFormat, utcToZonedTime} from 'date-fns-tz'; import {Str} from 'expensify-common'; import isEmpty from 'lodash/isEmpty'; @@ -55,11 +56,13 @@ import {prepareDraftComment} from '@libs/DraftCommentUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as Environment from '@libs/Environment/Environment'; import * as ErrorUtils from '@libs/ErrorUtils'; +import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector'; import isPublicScreenRoute from '@libs/isPublicScreenRoute'; import * as Localize from '@libs/Localize'; import Log from '@libs/Log'; import {registerPaginationConfig} from '@libs/Middleware/Pagination'; -import Navigation from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import {isOnboardingFlowName} from '@libs/NavigationUtils'; import type {NetworkStatus} from '@libs/NetworkConnection'; import LocalNotification from '@libs/Notification/LocalNotification'; import Parser from '@libs/Parser'; @@ -2541,28 +2544,47 @@ function openReportFromDeepLink(url: string) { // Navigate to the report after sign-in/sign-up. InteractionManager.runAfterInteractions(() => { Session.waitForUserSignIn().then(() => { - Navigation.waitForProtectedRoutes().then(() => { - if (route && Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(route)) { - Session.signOutAndRedirectToSignIn(true); - return; - } - - // We don't want to navigate to the exitTo route when creating a new workspace from a deep link, - // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate, - // which is already called when AuthScreens mounts. - if (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) { - return; - } - - if (shouldSkipDeepLinkNavigation(route)) { - return; - } - - if (isAuthenticated) { - return; - } - - Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH); + Onyx.connect({ + key: ONYXKEYS.NVP_ONBOARDING, + callback: (onboarding) => { + Navigation.waitForProtectedRoutes().then(() => { + if (route && Session.isAnonymousUser() && !Session.canAnonymousUserAccessRoute(route)) { + Session.signOutAndRedirectToSignIn(true); + return; + } + + // We don't want to navigate to the exitTo route when creating a new workspace from a deep link, + // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate, + // which is already called when AuthScreens mounts. + if (new URL(url).searchParams.get('exitTo') === ROUTES.WORKSPACE_NEW) { + return; + } + + if (shouldSkipDeepLinkNavigation(route)) { + return; + } + + const state = navigationRef.getRootState(); + const currentFocusedRoute = findFocusedRoute(state); + const hasCompletedGuidedSetupFlow = hasCompletedGuidedSetupFlowSelector(onboarding); + + // We need skip deeplinking if the user hasn't completed the guided setup flow. + if (!hasCompletedGuidedSetupFlow) { + return; + } + + if (isOnboardingFlowName(currentFocusedRoute?.name)) { + Welcome.setOnboardingErrorMessage(Localize.translateLocal('onboarding.purpose.errorBackButton')); + return; + } + + if (isAuthenticated) { + return; + } + + Navigation.navigate(route as Route, CONST.NAVIGATION.ACTION_TYPE.PUSH); + }); + }, }); }); }); diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index a90c386d02b6..b592424cfcdf 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -11,7 +11,8 @@ import ROUTES from '@src/ROUTES'; import type Onboarding from '@src/types/onyx/Onboarding'; import type TryNewDot from '@src/types/onyx/TryNewDot'; -let onboarding: Onboarding | [] | undefined; +type OnboardingData = Onboarding | [] | undefined; + let isLoadingReportData = true; let tryNewDotData: TryNewDot | undefined; @@ -30,8 +31,8 @@ let isServerDataReadyPromise = new Promise((resolve) => { resolveIsReadyPromise = resolve; }); -let resolveOnboardingFlowStatus: (value?: Promise) => void | undefined; -let isOnboardingFlowStatusKnownPromise = new Promise((resolve) => { +let resolveOnboardingFlowStatus: (value?: OnboardingData) => void; +let isOnboardingFlowStatusKnownPromise = new Promise((resolve) => { resolveOnboardingFlowStatus = resolve; }); @@ -45,7 +46,7 @@ function onServerDataReady(): Promise { } function isOnboardingFlowCompleted({onCompleted, onNotCompleted}: HasCompletedOnboardingFlowProps) { - isOnboardingFlowStatusKnownPromise.then(() => { + isOnboardingFlowStatusKnownPromise.then((onboarding) => { if (Array.isArray(onboarding) || onboarding?.hasCompletedGuidedSetupFlow === undefined) { return; } @@ -102,23 +103,7 @@ function handleHybridAppOnboarding() { } /** - * Check that a few requests have completed so that the welcome action can proceed: - * - * - Whether we are a first time new expensify user - * - Whether we have loaded all policies the server knows about - * - Whether we have loaded all reports the server knows about - * Check if onboarding data is ready in order to check if the user has completed onboarding or not - */ -function checkOnboardingDataReady() { - if (onboarding === undefined) { - return; - } - - resolveOnboardingFlowStatus?.(); -} - -/** - * Check if user dismissed modal and if report data are loaded + * Check if report data are loaded */ function checkServerDataReady() { if (isLoadingReportData) { @@ -143,6 +128,10 @@ function setOnboardingPurposeSelected(value: OnboardingPurposeType) { Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, value ?? null); } +function setOnboardingErrorMessage(value: string) { + Onyx.set(ONYXKEYS.ONBOARDING_ERROR_MESSAGE, value ?? null); +} + function setOnboardingAdminsChatReportID(adminsChatReportID?: string) { Onyx.set(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID, adminsChatReportID ?? null); } @@ -186,9 +175,7 @@ Onyx.connect({ return; } - onboarding = value; - - checkOnboardingDataReady(); + resolveOnboardingFlowStatus(value); }, }); @@ -213,10 +200,9 @@ function resetAllChecks() { isServerDataReadyPromise = new Promise((resolve) => { resolveIsReadyPromise = resolve; }); - isOnboardingFlowStatusKnownPromise = new Promise((resolve) => { + isOnboardingFlowStatusKnownPromise = new Promise((resolve) => { resolveOnboardingFlowStatus = resolve; }); - onboarding = undefined; isLoadingReportData = true; } @@ -229,4 +215,5 @@ export { setOnboardingPolicyID, completeHybridAppOnboarding, handleHybridAppOnboarding, + setOnboardingErrorMessage, }; diff --git a/src/libs/hasCompletedGuidedSetupFlowSelector.ts b/src/libs/hasCompletedGuidedSetupFlowSelector.ts new file mode 100644 index 000000000000..83cde0a0be8c --- /dev/null +++ b/src/libs/hasCompletedGuidedSetupFlowSelector.ts @@ -0,0 +1,12 @@ +import type {OnyxValue} from 'react-native-onyx'; +import type ONYXKEYS from '@src/ONYXKEYS'; + +function hasCompletedGuidedSetupFlowSelector(onboarding: OnyxValue): boolean { + // onboarding is an array for old accounts and accounts created from olddot + if (Array.isArray(onboarding)) { + return true; + } + return onboarding?.hasCompletedGuidedSetupFlow ?? false; +} + +export default hasCompletedGuidedSetupFlowSelector; diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index f5bd14ed7aa1..52e2d817e6db 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -12,7 +12,6 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape'; import useLocalize from '@hooks/useLocalize'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -46,7 +45,9 @@ function BaseOnboardingPersonalDetails({ const [shouldValidateOnChange, setShouldValidateOnChange] = useState(false); const {accountID} = useSession(); - useDisableModalDismissOnEscape(); + useEffect(() => { + Welcome.setOnboardingErrorMessage(''); + }, []); const completeEngagement = useCallback( (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => { diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx index 03a4b790bc5f..7304c1822ae9 100644 --- a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx +++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx @@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; @@ -13,7 +13,6 @@ import MenuItemList from '@components/MenuItemList'; import OfflineIndicator from '@components/OfflineIndicator'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import Text from '@components/Text'; -import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape'; import useLocalize from '@hooks/useLocalize'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useTheme from '@hooks/useTheme'; @@ -28,7 +27,8 @@ import type {OnboardingPurposeType} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps} from './types'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import type {BaseOnboardingPurposeProps} from './types'; const menuIcons = { [CONST.ONBOARDING_CHOICES.EMPLOYER]: Illustrations.ReceiptUpload, @@ -38,15 +38,15 @@ const menuIcons = { [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: Illustrations.Binoculars, }; -function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, onboardingPurposeSelected}: BaseOnboardingPurposeProps) { +function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight}: BaseOnboardingPurposeProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useOnboardingLayout(); const [selectedPurpose, setSelectedPurpose] = useState(undefined); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const theme = useTheme(); - - useDisableModalDismissOnEscape(); + const [onboardingPurposeSelected, onboardingPurposeSelectedResult] = useOnyx(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED); + const [onboardingErrorMessage, onboardingErrorMessageResult] = useOnyx(ONYXKEYS.ONBOARDING_ERROR_MESSAGE); const PurposeFooterInstance = ; @@ -83,8 +83,6 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS); }, [selectedPurpose]); - const [errorMessage, setErrorMessage] = useState(''); - const menuItems: MenuItemProps[] = Object.values(CONST.ONBOARDING_CHOICES).map((choice) => { const translationKey = `onboarding.purpose.${choice}` as const; const isSelected = selectedPurpose === choice; @@ -103,7 +101,7 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on numberOfLinesTitle: 0, onPress: () => { Welcome.setOnboardingPurposeSelected(choice); - setErrorMessage(''); + Welcome.setOnboardingErrorMessage(''); }, }; }); @@ -111,15 +109,18 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on const handleOuterClick = useCallback(() => { if (!selectedPurpose) { - setErrorMessage(translate('onboarding.purpose.errorSelection')); + Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorSelection')); } else { - setErrorMessage(translate('onboarding.purpose.errorContinue')); + Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorContinue')); } - }, [selectedPurpose, setErrorMessage, translate]); + }, [selectedPurpose, translate]); const onboardingLocalRef = useRef(null); useImperativeHandle(isFocused ? OnboardingRefManager.ref : onboardingLocalRef, () => ({handleOuterClick}), [handleOuterClick]); + if (isLoadingOnyxValue(onboardingPurposeSelectedResult, onboardingErrorMessageResult)) { + return null; + } return ( {({safeAreaPaddingBottomStyle}) => ( @@ -148,14 +149,14 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on buttonText={translate('common.continue')} onSubmit={() => { if (!selectedPurpose) { - setErrorMessage(translate('onboarding.purpose.errorSelection')); + Welcome.setOnboardingErrorMessage(translate('onboarding.purpose.errorSelection')); return; } - setErrorMessage(''); + Welcome.setOnboardingErrorMessage(''); saveAndNavigate(); }} - message={errorMessage} - isAlertVisible={!!errorMessage} + message={onboardingErrorMessage} + isAlertVisible={!!onboardingErrorMessage} containerStyles={[styles.w100, styles.mb5, styles.mh0, paddingHorizontal]} /> @@ -166,10 +167,6 @@ function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, on BaseOnboardingPurpose.displayName = 'BaseOnboardingPurpose'; -export default withOnyx({ - onboardingPurposeSelected: { - key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, - }, -})(BaseOnboardingPurpose); +export default BaseOnboardingPurpose; export type {BaseOnboardingPurposeProps}; diff --git a/src/pages/OnboardingPurpose/types.ts b/src/pages/OnboardingPurpose/types.ts index 8c8f11503f1a..17970dbab9a6 100644 --- a/src/pages/OnboardingPurpose/types.ts +++ b/src/pages/OnboardingPurpose/types.ts @@ -1,20 +1,11 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import type {OnboardingPurposeType} from '@src/CONST'; - type OnboardingPurposeProps = Record; -type BaseOnboardingPurposeOnyxProps = { - /** Saved onboarding purpose selected by the user */ - onboardingPurposeSelected: OnyxEntry; -}; - -type BaseOnboardingPurposeProps = OnboardingPurposeProps & - BaseOnboardingPurposeOnyxProps & { - /* Whether to use native styles tailored for native devices */ - shouldUseNativeStyles: boolean; +type BaseOnboardingPurposeProps = OnboardingPurposeProps & { + /* Whether to use native styles tailored for native devices */ + shouldUseNativeStyles: boolean; - /** Whether to use the maxHeight (true) or use the 100% of the height (false) */ - shouldEnableMaxHeight: boolean; - }; + /** Whether to use the maxHeight (true) or use the 100% of the height (false) */ + shouldEnableMaxHeight: boolean; +}; -export type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps, OnboardingPurposeProps}; +export type {BaseOnboardingPurposeProps, OnboardingPurposeProps}; diff --git a/src/pages/OnboardingWork/BaseOnboardingWork.tsx b/src/pages/OnboardingWork/BaseOnboardingWork.tsx index 9b8824300d30..14f9223f6c67 100644 --- a/src/pages/OnboardingWork/BaseOnboardingWork.tsx +++ b/src/pages/OnboardingWork/BaseOnboardingWork.tsx @@ -9,7 +9,6 @@ import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import useDisableModalDismissOnEscape from '@hooks/useDisableModalDismissOnEscape'; import useLocalize from '@hooks/useLocalize'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -33,8 +32,6 @@ function BaseOnboardingWork({shouldUseNativeStyles, onboardingPurposeSelected, o const {isSmallScreenWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useOnboardingLayout(); - useDisableModalDismissOnEscape(); - const completeEngagement = useCallback( (values: FormOnyxValues<'onboardingWorkForm'>) => { if (!onboardingPurposeSelected) { diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index b10cae2e7736..ca30eb10b065 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -39,6 +39,10 @@ jest.mock('../../src/components/ConfirmedRoute.tsx'); TestHelper.setupApp(); TestHelper.setupGlobalFetchMock(); +beforeEach(() => { + Onyx.set(ONYXKEYS.NVP_ONBOARDING, {hasCompletedGuidedSetupFlow: true}); +}); + function scrollUpToRevealNewMessagesBadge() { const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages'); fireEvent.scroll(screen.getByLabelText(hintText), { From 461195df2375a7ffe9377e91ed2a601eb003dfa1 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 12 Jul 2024 11:18:50 +0200 Subject: [PATCH 13/54] Fix logging issues --- src/libs/Navigation/NavigationRoot.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index a225831b56ff..f1b78bd2e109 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -87,7 +87,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N const initialState = useMemo(() => { // If the user haven't completed the flow, we want to always redirect them to the onboarding flow. - if (!hasCompletedGuidedSetupFlow) { + // We also make sure that the user is authenticated. + if (!hasCompletedGuidedSetupFlow && authenticated) { const {adaptedState} = getAdaptedStateFromPath(ROUTES.ONBOARDING_ROOT, linkingConfig.config); return adaptedState; } From cea572f1188cdc5b697011f98e22d702a3bdb4a1 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jul 2024 11:39:38 +0200 Subject: [PATCH 14/54] Fix going to Public room shows onboarding modal --- src/libs/Navigation/NavigationRoot.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index f1b78bd2e109..e6da17cf42a5 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -80,12 +80,17 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N const currentReportIDValue = useCurrentReportID(); const {isSmallScreenWidth} = useWindowDimensions(); const {setActiveWorkspaceID} = useActiveWorkspace(); + const [user] = useOnyx(ONYXKEYS.USER); const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { selector: hasCompletedGuidedSetupFlowSelector, }); const initialState = useMemo(() => { + if (!user || user.isFromPublicDomain) { + return; + } + // If the user haven't completed the flow, we want to always redirect them to the onboarding flow. // We also make sure that the user is authenticated. if (!hasCompletedGuidedSetupFlow && authenticated) { From e76f95a407177d52e91ef692a22c7967547cbcca Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 15 Jul 2024 13:15:02 +0200 Subject: [PATCH 15/54] Implement limit update confirm modal --- src/languages/en.ts | 5 ++ src/languages/es.ts | 6 ++ .../WorkspaceEditCardLimitPage.tsx | 69 ++++++++++++++++--- 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 6a62fd62d1f6..0e87f9375649 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2638,6 +2638,11 @@ export default { 'We consider a number of factors when calculating your remaining limit: your tenure as a customer, the business-related information you provided during signup, and the available cash in your business bank account. Your remaining limit can fluctuate on a daily basis.', cashBack: 'Cash back', cashBackDescription: 'Cash back balance is based on settled monthly Expensify Card spend across your workspace.', + changeCardLimit: 'Change card limit', + changeLimit: 'Change limit', + smartLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined until you approve more expenses on the card.`, + monthlyLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined until next month.`, + fixedLimitWarning: (limit: string) => `If you change this card’s limit to ${limit}, new transactions will be declined.`, }, categories: { deleteCategories: 'Delete categories', diff --git a/src/languages/es.ts b/src/languages/es.ts index b37a79b1bf00..0f0f830475b4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2684,6 +2684,12 @@ export default { 'A la hora de calcular tu límite restante, tenemos en cuenta una serie de factores: su antigüedad como cliente, la información relacionada con tu negocio que nos facilitaste al darte de alta y el efectivo disponible en tu cuenta bancaria comercial. Tu límite restante puede fluctuar a diario.', cashBack: 'Reembolso', cashBackDescription: 'El saldo de devolución se basa en el gasto mensual realizado con la tarjeta Expensify en tu espacio de trabajo.', + changeCardLimit: 'Modificar el límite de la tarjeta', + changeLimit: 'Modificar límite', + smartLimitWarning: (limit: string) => + `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta que apruebes antiguos gastos de la tarjeta.`, + monthlyLimitWarning: (limit: string) => `Si cambias el límite de esta tarjeta a ${limit}, las nuevas transacciones serán rechazadas hasta el próximo mes.`, + fixedLimitWarning: (limit: string) => `Si cambias el límite de esta tarjeta a ${limit}, se rechazarán las nuevas transacciones.`, }, categories: { deleteCategories: 'Eliminar categorías', diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 97d7eb462501..31a23ddf974b 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -1,7 +1,8 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import AmountForm from '@components/AmountForm'; +import ConfirmModal from '@components/ConfirmModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -16,9 +17,11 @@ import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/EditExpensifyCardLimit'; +type ConfirmationWarningTranslationPaths = 'workspace.expensifyCard.smartLimitWarning' | 'workspace.expensifyCard.monthlyLimitWarning' | 'workspace.expensifyCard.fixedLimitWarning'; + // TODO: remove when Onyx data is available const mockedCard = { accountID: 885646, @@ -39,11 +42,42 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); const styles = useThemeStyles(); + const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`); const card = cardsList?.[cardID] ?? mockedCard; - const submit = (values: FormOnyxValues) => {}; + const getPromptTextKey = useMemo((): ConfirmationWarningTranslationPaths => { + switch (card.nameValuePairs?.limitType) { + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART: + return 'workspace.expensifyCard.smartLimitWarning'; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED: + return 'workspace.expensifyCard.fixedLimitWarning'; + case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY: + return 'workspace.expensifyCard.monthlyLimitWarning'; + default: + return 'workspace.expensifyCard.fixedLimitWarning'; + } + }, [card.nameValuePairs?.limitType]); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const updateCardLimit = (newLimit: string) => { + setIsConfirmModalVisible(false); + // TODO: add API call when it's supported https://github.com/Expensify/Expensify/issues/407831 + }; + + const submit = (values: FormOnyxValues) => { + const currentLimit = card.nameValuePairs?.limit ?? 0; + const currentRemainingLimit = currentLimit - card.availableSpend; + const newRemainingLimit = Number(values[INPUT_IDS.LIMIT]) - currentRemainingLimit; + + if (newRemainingLimit <= 0) { + setIsConfirmModalVisible(true); + return; + } + + updateCardLimit(values[INPUT_IDS.LIMIT]); + }; const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { @@ -80,13 +114,28 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { enabledWhenOffline validate={validate} > - + {({inputValues}) => ( + <> + + updateCardLimit(inputValues[INPUT_IDS.LIMIT])} + onCancel={() => setIsConfirmModalVisible(false)} + prompt={translate(getPromptTextKey, CurrencyUtils.convertToDisplayString(Number(inputValues[INPUT_IDS.LIMIT]) * 100, CONST.CURRENCY.USD))} + confirmText={translate('workspace.expensifyCard.changeLimit')} + cancelText={translate('common.cancel')} + danger + shouldEnableNewFocusManagement + /> + + )} ); From ff14bcbaf8634b3d45c2749714ee5c8ad78e140e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 15 Jul 2024 14:29:25 +0200 Subject: [PATCH 16/54] Add AccessOrNotFound wrapper --- .../WorkspaceEditCardLimitPage.tsx | 91 ++++++++++--------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 31a23ddf974b..ba870d8cfece 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -15,6 +15,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -94,50 +95,56 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { ); return ( - - Navigation.goBack()} - /> - - {({inputValues}) => ( - <> - - updateCardLimit(inputValues[INPUT_IDS.LIMIT])} - onCancel={() => setIsConfirmModalVisible(false)} - prompt={translate(getPromptTextKey, CurrencyUtils.convertToDisplayString(Number(inputValues[INPUT_IDS.LIMIT]) * 100, CONST.CURRENCY.USD))} - confirmText={translate('workspace.expensifyCard.changeLimit')} - cancelText={translate('common.cancel')} - danger - shouldEnableNewFocusManagement - /> - - )} - - + Navigation.goBack()} + /> + + {({inputValues}) => ( + <> + + updateCardLimit(inputValues[INPUT_IDS.LIMIT])} + onCancel={() => setIsConfirmModalVisible(false)} + prompt={translate(getPromptTextKey, CurrencyUtils.convertToDisplayString(Number(inputValues[INPUT_IDS.LIMIT]) * 100, CONST.CURRENCY.USD))} + confirmText={translate('workspace.expensifyCard.changeLimit')} + cancelText={translate('common.cancel')} + danger + shouldEnableNewFocusManagement + /> + + )} + + + ); } From f9812476ee6a4629bda6d358f54f4176a4925360 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 16 Jul 2024 09:22:21 +0200 Subject: [PATCH 17/54] Minor improvements --- .../Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts | 7 ++++++- .../workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx | 3 +++ .../expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx | 3 ++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index e735287758ca..e6a84e906978 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -161,7 +161,12 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE, SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE, ], - [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT, SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS, SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT], + [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [ + SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, + SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT, + SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS, + SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT, + ], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index ba870d8cfece..758d34bd6347 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -64,7 +64,10 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const updateCardLimit = (newLimit: string) => { setIsConfirmModalVisible(false); + // TODO: add API call when it's supported https://github.com/Expensify/Expensify/issues/407831 + + Navigation.goBack(); }; const submit = (values: FormOnyxValues) => { diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 6934776e44ba..c1e9e0d0e0b2 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -24,6 +24,7 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; // TODO: remove when Onyx data is available @@ -115,7 +116,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail description={translate('workspace.expensifyCard.cardLimit')} title={formattedLimit} shouldShowRightIcon - onPress={() => {}} // TODO: navigate to Edit card limit page https://github.com/Expensify/App/issues/44326 + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_LIMIT.getRoute(policyID, cardID))} /> Date: Tue, 16 Jul 2024 09:35:49 +0200 Subject: [PATCH 18/54] Fix limit check --- .../workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 758d34bd6347..76f07f5070ff 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -73,7 +73,7 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { const submit = (values: FormOnyxValues) => { const currentLimit = card.nameValuePairs?.limit ?? 0; const currentRemainingLimit = currentLimit - card.availableSpend; - const newRemainingLimit = Number(values[INPUT_IDS.LIMIT]) - currentRemainingLimit; + const newRemainingLimit = Number(values[INPUT_IDS.LIMIT]) * 100 - currentRemainingLimit; if (newRemainingLimit <= 0) { setIsConfirmModalVisible(true); From bce8f206e4443cc0655d0ed9346451d0349e183b Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 16 Jul 2024 12:48:40 +0200 Subject: [PATCH 19/54] Update route to follow docs --- src/ROUTES.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ee4dbc116fad..6fc5b24a8855 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -852,8 +852,8 @@ const ROUTES = { getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo), }, WORKSPACE_EXPENSIFY_CARD_LIMIT: { - route: 'settings/workspaces/:policyID/expensify-card/:cardID/limit', - getRoute: (policyID: string, cardID: string) => `settings/workspaces/${policyID}/expensify-card/${cardID}/limit` as const, + route: 'settings/workspaces/:policyID/expensify-card/:cardID/edit/limit', + getRoute: (policyID: string, cardID: string) => `settings/workspaces/${policyID}/expensify-card/${cardID}/edit/limit` as const, }, WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { route: 'settings/workspaces/:policyID/expensify-card/issue-new', From 5d8e5b50f45569a4a6cbba338a90f9cf5d6d681e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 16 Jul 2024 12:56:51 +0200 Subject: [PATCH 20/54] Update form const names --- src/ONYXKEYS.ts | 6 +++--- .../workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 62fb3c9c3e4f..ca0b4ab3fb25 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -561,8 +561,8 @@ const ONYXKEYS = { SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', - EDIT_EXPENSIFY_CARD_LIMIT: 'editExpensifyCardLimit', - EDIT_EXPENSIFY_CARD_LIMIT_DRAFT: 'editExpensifyCardLimitDraft', + EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit', + EDIT_EXPENSIFY_CARD_LIMIT_DRAFT_FORM: 'editExpensifyCardLimitDraft', SAGE_INTACCT_CREDENTIALS_FORM: 'sageIntacctCredentialsForm', SAGE_INTACCT_CREDENTIALS_FORM_DRAFT: 'sageIntacctCredentialsFormDraft', NETSUITE_CUSTOM_FIELD_FORM: 'netSuiteCustomFieldForm', @@ -639,7 +639,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; - [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT]: FormTypes.EditExpensifyCardLimit; + [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimit; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm; diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 76f07f5070ff..51d472dfc9fb 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -70,7 +70,7 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { Navigation.goBack(); }; - const submit = (values: FormOnyxValues) => { + const submit = (values: FormOnyxValues) => { const currentLimit = card.nameValuePairs?.limit ?? 0; const currentRemainingLimit = currentLimit - card.availableSpend; const newRemainingLimit = Number(values[INPUT_IDS.LIMIT]) * 100 - currentRemainingLimit; @@ -84,7 +84,7 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { }; const validate = useCallback( - (values: FormOnyxValues): FormInputErrors => { + (values: FormOnyxValues): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.LIMIT]); // We only want integers to be sent as the limit @@ -114,7 +114,7 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { onBackButtonPress={() => Navigation.goBack()} /> Date: Tue, 16 Jul 2024 13:38:03 +0200 Subject: [PATCH 21/54] Add EditExpensifyCardNameForm type --- src/ONYXKEYS.ts | 3 +++ src/types/form/EditExpensifyCardNameForm.ts | 13 +++++++++++++ src/types/form/index.ts | 1 + 3 files changed, 17 insertions(+) create mode 100644 src/types/form/EditExpensifyCardNameForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b06b05dac7e1..0b137466a715 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -561,6 +561,8 @@ const ONYXKEYS = { SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', + EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName', + EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft', SAGE_INTACCT_CREDENTIALS_FORM: 'sageIntacctCredentialsForm', SAGE_INTACCT_CREDENTIALS_FORM_DRAFT: 'sageIntacctCredentialsFormDraft', NETSUITE_CUSTOM_FIELD_FORM: 'netSuiteCustomFieldForm', @@ -637,6 +639,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; + [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm; diff --git a/src/types/form/EditExpensifyCardNameForm.ts b/src/types/form/EditExpensifyCardNameForm.ts new file mode 100644 index 000000000000..197ba70801b6 --- /dev/null +++ b/src/types/form/EditExpensifyCardNameForm.ts @@ -0,0 +1,13 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + NAME: 'name', +} as const; + +type InputID = ValueOf; + +type EditExpensifyCardNameForm = Form; + +export type {EditExpensifyCardNameForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 450de82a60cf..dd34c1afaf4d 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -10,6 +10,7 @@ export type {HomeAddressForm} from './HomeAddressForm'; export type {IKnowTeacherForm} from './IKnowTeacherForm'; export type {IntroSchoolPrincipalForm} from './IntroSchoolPrincipalForm'; export type {IssueNewExpensifyCardForm} from './IssueNewExpensifyCardForm'; +export type {EditExpensifyCardNameForm} from './EditExpensifyCardNameForm'; export type {LegalNameForm} from './LegalNameForm'; export type {MoneyRequestAmountForm} from './MoneyRequestAmountForm'; export type {MoneyRequestDateForm} from './MoneyRequestDateForm'; From fe4e2caa45297cc3d372b36b177ebd4d7a5a6113 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 16 Jul 2024 13:41:55 +0200 Subject: [PATCH 22/54] Rename file --- src/ONYXKEYS.ts | 2 +- .../workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx | 2 +- ...ditExpensifyCardLimit.ts => EditExpensifyCardLimitForm.ts} | 4 ++-- src/types/form/index.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/types/form/{EditExpensifyCardLimit.ts => EditExpensifyCardLimitForm.ts} (62%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ca0b4ab3fb25..44c3737caca5 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -639,7 +639,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; - [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimit; + [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_LIST_ADD_FORM]: FormTypes.NetSuiteCustomFieldForm; diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 51d472dfc9fb..f2e421f7141b 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -19,7 +19,7 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import INPUT_IDS from '@src/types/form/EditExpensifyCardLimit'; +import INPUT_IDS from '@src/types/form/EditExpensifyCardLimitForm'; type ConfirmationWarningTranslationPaths = 'workspace.expensifyCard.smartLimitWarning' | 'workspace.expensifyCard.monthlyLimitWarning' | 'workspace.expensifyCard.fixedLimitWarning'; diff --git a/src/types/form/EditExpensifyCardLimit.ts b/src/types/form/EditExpensifyCardLimitForm.ts similarity index 62% rename from src/types/form/EditExpensifyCardLimit.ts rename to src/types/form/EditExpensifyCardLimitForm.ts index b651a617c20a..8958999cfd7c 100644 --- a/src/types/form/EditExpensifyCardLimit.ts +++ b/src/types/form/EditExpensifyCardLimitForm.ts @@ -7,7 +7,7 @@ const INPUT_IDS = { type InputID = ValueOf; -type EditExpensifyCardLimit = Form; +type EditExpensifyCardLimitForm = Form; -export type {EditExpensifyCardLimit}; +export type {EditExpensifyCardLimitForm}; export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 0a88f2d6c26a..01db1729a354 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -60,5 +60,5 @@ export type {SageIntactCredentialsForm} from './SageIntactCredentialsForm'; export type {NetSuiteCustomFieldForm} from './NetSuiteCustomFieldForm'; export type {NetSuiteTokenInputForm} from './NetSuiteTokenInputForm'; export type {NetSuiteCustomFormIDForm} from './NetSuiteCustomFormIDForm'; -export type {EditExpensifyCardLimit} from './EditExpensifyCardLimit'; +export type {EditExpensifyCardLimitForm} from './EditExpensifyCardLimitForm'; export type {default as Form} from './Form'; From d77137975130af4572f28940b439d0c65118e673 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 16 Jul 2024 14:37:01 +0200 Subject: [PATCH 23/54] Implement WorkspaceEditCardName page --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 7 +- src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 4 + .../WorkspaceEditCardNamePage.tsx | 97 +++++++++++++++++++ .../WorkspaceExpensifyCardDetailsPage.tsx | 3 +- 8 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2f3372144610..103da5244cc3 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -851,6 +851,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/expensify-card/:cardID', getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo), }, + WORKSPACE_EXPENSIFY_CARD_NAME: { + route: 'settings/workspaces/:policyID/expensify-card/:cardID/edit/name', + getRoute: (policyID: string, cardID: string) => `settings/workspaces/${policyID}/expensify-card/${cardID}/edit/name` as const, + }, WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: { route: 'settings/workspaces/:policyID/expensify-card/issue-new', getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b2f1ee002669..6c28ca6cf8e0 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -344,6 +344,7 @@ const SCREENS = { EXPENSIFY_CARD: 'Workspace_ExpensifyCard', EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details', EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New', + EXPENSIFY_CARD_NAME: 'Workspace_ExpensifyCard_Name', EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount', BILLS: 'Workspace_Bills', INVOICES: 'Workspace_Invoices', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 6c8873b1f0bf..3bde65cd9d65 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -411,6 +411,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage').default, + [SCREENS.WORKSPACE.EXPENSIFY_CARD_NAME]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceEditCardNamePage').default, [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default, [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default, [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: () => require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index f03b837cb9fc..74adda99ee36 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -161,7 +161,12 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE, SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE, ], - [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT, SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS], + [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [ + SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, + SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT, + SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS, + SCREENS.WORKSPACE.EXPENSIFY_CARD_NAME, + ], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index e5b4683a617b..24be9083cc17 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -465,6 +465,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: { path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.route, }, + [SCREENS.WORKSPACE.EXPENSIFY_CARD_NAME]: { + path: ROUTES.WORKSPACE_EXPENSIFY_CARD_NAME.route, + }, [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: { path: ROUTES.WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b66e74e384b3..8cffc3e426e1 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -661,6 +661,10 @@ type SettingsNavigatorParamList = { cardID: string; backTo?: Routes; }; + [SCREENS.WORKSPACE.EXPENSIFY_CARD_NAME]: { + policyID: string; + cardID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx new file mode 100644 index 000000000000..b584029891a8 --- /dev/null +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx @@ -0,0 +1,97 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import Navigation from '@navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/EditExpensifyCardNameForm'; + +// TODO: remove when Onyx data is available +const mockedCard = { + accountID: 885646, + availableSpend: 1000, + nameValuePairs: { + cardTitle: 'Test 1', + isVirtual: true, + limit: 2000, + limitType: CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART, + }, + lastFourPAN: '1234', +}; + +type WorkspaceEditCardNamePageProps = StackScreenProps; + +function WorkspaceEditCardNamePage({route}: WorkspaceEditCardNamePageProps) { + const {policyID, cardID} = route.params; + + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const styles = useThemeStyles(); + + const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`); + const card = cardsList?.[cardID] ?? mockedCard; + + const submit = (values: FormOnyxValues) => { + // TODO: add API call when it's supported https://github.com/Expensify/Expensify/issues/407832 + Navigation.goBack(); + }; + + const validate = (values: FormOnyxValues): FormInputErrors => + ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.NAME]); + + return ( + + + Navigation.goBack()} + /> + + + + + + ); +} + +WorkspaceEditCardNamePage.displayName = 'WorkspaceEditCardNamePage'; + +export default WorkspaceEditCardNamePage; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 6934776e44ba..dc07d17c36ee 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -24,6 +24,7 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; // TODO: remove when Onyx data is available @@ -127,7 +128,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail description={translate('workspace.card.issueNewCard.cardName')} title={card.nameValuePairs?.cardTitle} shouldShowRightIcon - onPress={() => {}} // TODO: navigate to Edit card name page https://github.com/Expensify/App/issues/44327 + onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_NAME.getRoute(policyID, cardID))} /> Date: Tue, 16 Jul 2024 14:50:12 +0200 Subject: [PATCH 24/54] Lint fix --- src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx index b584029891a8..2550aa7249f0 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx @@ -44,6 +44,7 @@ function WorkspaceEditCardNamePage({route}: WorkspaceEditCardNamePageProps) { const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`); const card = cardsList?.[cardID] ?? mockedCard; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const submit = (values: FormOnyxValues) => { // TODO: add API call when it's supported https://github.com/Expensify/Expensify/issues/407832 Navigation.goBack(); From 3366882ccd0668401b5cde0c864ad8fc51843f90 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 16 Jul 2024 17:48:33 +0200 Subject: [PATCH 25/54] Minor UI improvement --- src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx index 2550aa7249f0..1ef00c8c8700 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx @@ -61,7 +61,6 @@ function WorkspaceEditCardNamePage({route}: WorkspaceEditCardNamePageProps) { > From 5a99c7b75bb5c5d07b0e7183213b36b4748087c6 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 16 Jul 2024 17:52:48 +0200 Subject: [PATCH 26/54] Minor UI improvement --- src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index f2e421f7141b..39ba4b876492 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -105,7 +105,6 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { > From fc47ec98eb2f78077b5cb9e2574e307efdcd0637 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Jul 2024 09:37:18 +0200 Subject: [PATCH 27/54] Remove extra param --- .../workspace/expensifyCard/WorkspaceEditCardNamePage.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx index 1ef00c8c8700..51bbe440f5d2 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardNamePage.tsx @@ -64,10 +64,7 @@ function WorkspaceEditCardNamePage({route}: WorkspaceEditCardNamePageProps) { shouldEnablePickerAvoiding={false} shouldEnableMaxHeight > - Navigation.goBack()} - /> + Date: Wed, 17 Jul 2024 09:39:02 +0200 Subject: [PATCH 28/54] Remove extra param --- .../workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 39ba4b876492..43231592ae76 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -108,10 +108,7 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { shouldEnablePickerAvoiding={false} shouldEnableMaxHeight > - Navigation.goBack()} - /> + Date: Wed, 17 Jul 2024 15:36:17 +0200 Subject: [PATCH 29/54] Update calculations --- .../workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 43231592ae76..4b7b3285a083 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -72,10 +72,11 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { const submit = (values: FormOnyxValues) => { const currentLimit = card.nameValuePairs?.limit ?? 0; - const currentRemainingLimit = currentLimit - card.availableSpend; - const newRemainingLimit = Number(values[INPUT_IDS.LIMIT]) * 100 - currentRemainingLimit; + const currentSpend = currentLimit - card.availableSpend; + const newLimit = Number(values[INPUT_IDS.LIMIT]) * 100; + const newAvailableSpend = newLimit - currentSpend; - if (newRemainingLimit <= 0) { + if (newAvailableSpend <= 0) { setIsConfirmModalVisible(true); return; } From 8c0b80b2c31fdc676cb08df31ce1b76a9d1a4c23 Mon Sep 17 00:00:00 2001 From: Mahesh Vagicherla Date: Wed, 17 Jul 2024 23:45:30 +0530 Subject: [PATCH 30/54] wrap functions in usecallback & solve lint --- src/components/AvatarWithImagePicker.tsx | 63 +++++++++++++----------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 860e2ccf3636..919a4a67ebc6 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -195,15 +195,15 @@ function AvatarWithImagePicker({ /** * Check if the attachment extension is allowed. */ - const isValidExtension = (image: FileObject): boolean => { + const isValidExtension = useCallback((image: FileObject): boolean => { const {fileExtension} = FileUtils.splitExtensionFromFileName(image?.name ?? ''); return CONST.AVATAR_ALLOWED_EXTENSIONS.some((extension) => extension === fileExtension.toLowerCase()); - }; + }, []); /** * Check if the attachment size is less than allowed size. */ - const isValidSize = (image: FileObject): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE; + const isValidSize = useCallback((image: FileObject): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE, []); /** * Check if the attachment resolution matches constraints. @@ -216,37 +216,40 @@ function AvatarWithImagePicker({ /** * Validates if an image has a valid resolution and opens an avatar crop modal */ - const showAvatarCropModal = (image: FileObject) => { - if (!isValidExtension(image)) { - setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS}); - return; - } - if (!isValidSize(image)) { - setError('avatarWithImagePicker.sizeExceeded', {maxUploadSizeInMB: CONST.AVATAR_MAX_ATTACHMENT_SIZE / (1024 * 1024)}); - return; - } - - isValidResolution(image).then((isValid) => { - if (!isValid) { - setError('avatarWithImagePicker.resolutionConstraints', { - minHeightInPx: CONST.AVATAR_MIN_HEIGHT_PX, - minWidthInPx: CONST.AVATAR_MIN_WIDTH_PX, - maxHeightInPx: CONST.AVATAR_MAX_HEIGHT_PX, - maxWidthInPx: CONST.AVATAR_MAX_WIDTH_PX, - }); + const showAvatarCropModal = useCallback( + (image: FileObject) => { + if (!isValidExtension(image)) { + setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS}); + return; + } + if (!isValidSize(image)) { + setError('avatarWithImagePicker.sizeExceeded', {maxUploadSizeInMB: CONST.AVATAR_MAX_ATTACHMENT_SIZE / (1024 * 1024)}); return; } - setIsAvatarCropModalOpen(true); - setError(null, {}); - setIsMenuVisible(false); - setImageData({ - uri: image.uri ?? '', - name: image.name ?? '', - type: image.type ?? '', + isValidResolution(image).then((isValid) => { + if (!isValid) { + setError('avatarWithImagePicker.resolutionConstraints', { + minHeightInPx: CONST.AVATAR_MIN_HEIGHT_PX, + minWidthInPx: CONST.AVATAR_MIN_WIDTH_PX, + maxHeightInPx: CONST.AVATAR_MAX_HEIGHT_PX, + maxWidthInPx: CONST.AVATAR_MAX_WIDTH_PX, + }); + return; + } + + setIsAvatarCropModalOpen(true); + setError(null, {}); + setIsMenuVisible(false); + setImageData({ + uri: image.uri ?? '', + name: image.name ?? '', + type: image.type ?? '', + }); }); - }); - }; + }, + [isValidExtension, isValidSize], + ); const hideAvatarCropModal = () => { setIsAvatarCropModalOpen(false); From 83e1705dc8a98cf08b20c8eeb3d8450952bf4b30 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 18 Jul 2024 11:49:52 +0200 Subject: [PATCH 31/54] Hide extra error --- src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 4b7b3285a083..28f9950d6c5d 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -113,6 +113,7 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { Date: Thu, 18 Jul 2024 16:12:37 +0200 Subject: [PATCH 32/54] Add API request to update expensify card limit --- .../UpdateExpensifyCardLimitParams.ts | 7 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Card.ts | 71 ++++++++++++++++++- .../WorkspaceEditCardLimitPage.tsx | 4 +- 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 src/libs/API/parameters/UpdateExpensifyCardLimitParams.ts diff --git a/src/libs/API/parameters/UpdateExpensifyCardLimitParams.ts b/src/libs/API/parameters/UpdateExpensifyCardLimitParams.ts new file mode 100644 index 000000000000..d64094185a79 --- /dev/null +++ b/src/libs/API/parameters/UpdateExpensifyCardLimitParams.ts @@ -0,0 +1,7 @@ +type UpdateExpensifyCardLimitParams = { + authToken: string; + cardID: number; + limit: number; +}; + +export default UpdateExpensifyCardLimitParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 204fa01ed14c..688d835dec45 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -258,3 +258,4 @@ export type {default as UpdateNetSuiteCustomFormIDParams} from './UpdateNetSuite export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIntacctGenericTypeParams'; export type {default as UpdateNetSuiteCustomersJobsParams} from './UpdateNetSuiteCustomersJobsParams'; export type {default as CopyExistingPolicyConnectionParams} from './CopyExistingPolicyConnectionParams'; +export type {default as UpdateExpensifyCardLimitParams} from './UpdateExpensifyCardLimitParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 4ecddbdb7406..2e711dde6ea9 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -31,6 +31,7 @@ const WRITE_COMMANDS = { REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD: 'ReportVirtualExpensifyCardFraud', REQUEST_REPLACEMENT_EXPENSIFY_CARD: 'RequestReplacementExpensifyCard', ACTIVATE_PHYSICAL_EXPENSIFY_CARD: 'ActivatePhysicalExpensifyCard', + UPDATE_EXPENSIFY_CARD_LIMIT: 'UpdateExpensifyCardLimit', CHRONOS_REMOVE_OOO_EVENT: 'Chronos_RemoveOOOEvent', MAKE_DEFAULT_PAYMENT_METHOD: 'MakeDefaultPaymentMethod', ADD_PAYMENT_CARD: 'AddPaymentCard', @@ -333,6 +334,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD]: Parameters.ReportVirtualExpensifyCardFraudParams; [WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD]: Parameters.RequestReplacementExpensifyCardParams; [WRITE_COMMANDS.ACTIVATE_PHYSICAL_EXPENSIFY_CARD]: Parameters.ActivatePhysicalExpensifyCardParams; + [WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT]: Parameters.UpdateExpensifyCardLimitParams; [WRITE_COMMANDS.MAKE_DEFAULT_PAYMENT_METHOD]: Parameters.MakeDefaultPaymentMethodParams; [WRITE_COMMANDS.ADD_PAYMENT_CARD]: Parameters.AddPaymentCardParams; [WRITE_COMMANDS.DELETE_PAYMENT_CARD]: Parameters.DeletePaymentCardParams; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index b23493c08e8e..6ba0bfc0729a 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -1,8 +1,16 @@ import Onyx from 'react-native-onyx'; import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; -import type {ActivatePhysicalExpensifyCardParams, ReportVirtualExpensifyCardFraudParams, RequestReplacementExpensifyCardParams, RevealExpensifyCardDetailsParams} from '@libs/API/parameters'; +import type { + ActivatePhysicalExpensifyCardParams, + ReportVirtualExpensifyCardFraudParams, + RequestReplacementExpensifyCardParams, + RevealExpensifyCardDetailsParams, + UpdateExpensifyCardLimitParams, +} from '@libs/API/parameters'; import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import * as NetworkStore from '@libs/Network/NetworkStore'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card'; @@ -207,6 +215,66 @@ function clearIssueNewCardFlow() { }); } +function updateExpensifyCardLimit(policyID: string, cardID: number, newLimit: number, oldLimit?: number) { + const authToken = NetworkStore.getAuthToken(); + + if (!authToken) { + return; + } + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`, + value: { + [cardID]: { + nameValuePairs: { + limit: newLimit, + }, + isLoading: true, + errors: null, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`, + value: { + [cardID]: { + isLoading: false, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`, + value: { + [cardID]: { + nameValuePairs: { + limit: oldLimit, + }, + isLoading: false, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + ]; + + const parameters: UpdateExpensifyCardLimitParams = { + authToken, + cardID, + limit: newLimit, + }; + + API.write(WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT, parameters, {optimisticData, successData, failureData}); +} + export { requestReplacementExpensifyCard, activatePhysicalExpensifyCard, @@ -215,5 +283,6 @@ export { revealVirtualCardDetails, setIssueNewCardStepAndData, clearIssueNewCardFlow, + updateExpensifyCardLimit, }; export type {ReplacementReason}; diff --git a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx index 28f9950d6c5d..6dc6bb2a78ca 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceEditCardLimitPage.tsx @@ -16,6 +16,7 @@ import * as ValidationUtils from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Card from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -61,11 +62,10 @@ function WorkspaceEditCardLimitPage({route}: WorkspaceEditCardLimitPageProps) { } }, [card.nameValuePairs?.limitType]); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const updateCardLimit = (newLimit: string) => { setIsConfirmModalVisible(false); - // TODO: add API call when it's supported https://github.com/Expensify/Expensify/issues/407831 + Card.updateExpensifyCardLimit(policyID, Number(cardID), Number(newLimit) * 100, card.nameValuePairs?.limit); Navigation.goBack(); }; From a8d1933b65ebb15b588571e8e40cdb3ec64178b5 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 11:15:20 -0600 Subject: [PATCH 33/54] create commands --- src/CONST.ts | 23 ++++++++++--------- .../SelectionList/Search/ActionCell.tsx | 22 +++++++++++++----- .../parameters/ApproveMoneyRequestOnSearch.ts | 6 +++++ .../API/parameters/PayMoneyRequestOnSearch.ts | 6 +++++ .../parameters/SubmitMoneyRequestOnSearch.ts | 6 +++++ src/libs/API/types.ts | 6 +++++ src/libs/actions/Search.ts | 20 +++++++++++++++- 7 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 src/libs/API/parameters/ApproveMoneyRequestOnSearch.ts create mode 100644 src/libs/API/parameters/PayMoneyRequestOnSearch.ts create mode 100644 src/libs/API/parameters/SubmitMoneyRequestOnSearch.ts diff --git a/src/CONST.ts b/src/CONST.ts index dcef6d01f36c..37563cb032de 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5188,12 +5188,22 @@ const CONST = { REPORT: 'report', }, ACTION_TYPES: { - DONE: 'done', - PAID: 'paid', VIEW: 'view', REVIEW: 'review', + SUBMIT: 'submit', + APPROVE: 'approve', + PAY: 'pay', + DONE: 'done', + PAID: 'paid', + }, + BULK_ACTION_TYPES: { + DELETE: 'delete', HOLD: 'hold', UNHOLD: 'unhold', + SUBMIT: 'submit', + APPROVE: 'approve', + PAY: 'pay', + EXPORT: 'export', }, TRANSACTION_TYPE: { CASH: 'cash', @@ -5224,15 +5234,6 @@ const CONST = { ACTION: 'action', TAX_AMOUNT: 'taxAmount', }, - BULK_ACTION_TYPES: { - DELETE: 'delete', - HOLD: 'hold', - UNHOLD: 'unhold', - SUBMIT: 'submit', - APPROVE: 'approve', - PAY: 'pay', - EXPORT: 'export', - }, SYNTAX_OPERATORS: { AND: 'and', OR: 'or', diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 7888a8b26114..5a211c9a9398 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -19,10 +19,11 @@ import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; const actionTranslationsMap: Record = { view: 'common.view', review: 'common.review', + submit: 'common.submit', + approve: 'iou.approve', + pay: 'iou.pay', done: 'common.done', paid: 'iou.settledExpensify', - hold: 'iou.hold', - unhold: 'iou.unhold', }; type ActionCellProps = { @@ -56,10 +57,18 @@ function ActionCell({ return; } - if (action === CONST.SEARCH.ACTION_TYPES.HOLD) { - Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(CONST.SEARCH.TAB.ALL, transactionID)); - } else if (action === CONST.SEARCH.ACTION_TYPES.UNHOLD) { - SearchActions.unholdMoneyRequestOnSearch(currentSearchHash, [transactionID]); + if (action === CONST.SEARCH.ACTION_TYPES.SUBMIT) { + SearchActions.submitMoneyRequestOnSearch(currentSearchHash, [transactionID]); + return; + } + + if (action === CONST.SEARCH.ACTION_TYPES.APPROVE) { + SearchActions.approveMoneyRequestOnSearch(currentSearchHash, [transactionID]); + return; + } + + if (action === CONST.SEARCH.ACTION_TYPES.PAY) { + SearchActions.payMoneyRequestOnSearch(currentSearchHash, [transactionID]); } }, [action, currentSearchHash, transactionID]); @@ -124,6 +133,7 @@ function ActionCell({ text={text} onPress={onButtonPress} small + success pressOnEnter style={[styles.w100]} innerStyles={buttonInnerStyles} diff --git a/src/libs/API/parameters/ApproveMoneyRequestOnSearch.ts b/src/libs/API/parameters/ApproveMoneyRequestOnSearch.ts new file mode 100644 index 000000000000..a1d4ba329067 --- /dev/null +++ b/src/libs/API/parameters/ApproveMoneyRequestOnSearch.ts @@ -0,0 +1,6 @@ +type SubmitMoneyRequestOnSearchParams = { + hash: number; + reportIDList: string[]; +}; + +export default SubmitMoneyRequestOnSearchParams; diff --git a/src/libs/API/parameters/PayMoneyRequestOnSearch.ts b/src/libs/API/parameters/PayMoneyRequestOnSearch.ts new file mode 100644 index 000000000000..5eb49063b9fc --- /dev/null +++ b/src/libs/API/parameters/PayMoneyRequestOnSearch.ts @@ -0,0 +1,6 @@ +type PayMoneyRequestOnSearchParams = { + hash: number; + reportIDList: string[]; +}; + +export default PayMoneyRequestOnSearchParams; diff --git a/src/libs/API/parameters/SubmitMoneyRequestOnSearch.ts b/src/libs/API/parameters/SubmitMoneyRequestOnSearch.ts new file mode 100644 index 000000000000..c30ed4b946e6 --- /dev/null +++ b/src/libs/API/parameters/SubmitMoneyRequestOnSearch.ts @@ -0,0 +1,6 @@ +type ApproveMoneyRequestOnSearchParams = { + hash: number; + reportIDList: string[]; +}; + +export default ApproveMoneyRequestOnSearchParams; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 34366dfe0f52..4dfcc510d7b7 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -241,6 +241,9 @@ const WRITE_COMMANDS = { DELETE_MONEY_REQUEST_ON_SEARCH: 'DeleteMoneyRequestOnSearch', HOLD_MONEY_REQUEST_ON_SEARCH: 'HoldMoneyRequestOnSearch', UNHOLD_MONEY_REQUEST_ON_SEARCH: 'UnholdMoneyRequestOnSearch', + SUBMIT_MONEY_REQUEST_ON_SEARCH: 'SubmitMoneyRequestOnSearch', + APPROVE_MONEY_REQUEST_ON_SEARCH: 'ApproveMoneyRequestOnSearch', + PAY_MONEY_REQUEST_ON_SEARCH: 'PayMoneyRequestOnSearch', REQUEST_REFUND: 'User_RefundPurchase', UPDATE_NETSUITE_SUBSIDIARY: 'UpdateNetSuiteSubsidiary', CREATE_WORKSPACE_REPORT_FIELD: 'CreatePolicyReportField', @@ -556,6 +559,9 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; [WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams; [WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.UnholdMoneyRequestOnSearchParams; + [WRITE_COMMANDS.SUBMIT_MONEY_REQUEST_ON_SEARCH]: Parameters.SubmitMoneyRequestOnSearchParams; + [WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH]: Parameters.ApproveMoneyRequestOnSearchParams; + [WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH]: Parameters.PayMoneyRequestOnSearchParams; [WRITE_COMMANDS.REQUEST_REFUND]: null; [WRITE_COMMANDS.CONNECT_POLICY_TO_SAGE_INTACCT]: Parameters.ConnectPolicyToSageIntacctParams; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 969812b02b02..140d2e41e934 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -82,6 +82,24 @@ function unholdMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { API.write(WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData}); } +function submitMoneyRequestOnSearch(hash: number, reportIDList: string[]) { + const {optimisticData, finallyData} = getOnyxLoadingData(hash); + + API.write(WRITE_COMMANDS.SUBMIT_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData}); +} + +function approveMoneyRequestOnSearch(hash: number, reportIDList: string[]) { + const {optimisticData, finallyData} = getOnyxLoadingData(hash); + + API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData}); +} + +function payMoneyRequestOnSearch(hash: number, reportIDList: string[]) { + const {optimisticData, finallyData} = getOnyxLoadingData(hash); + + API.write(WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData}); +} + function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { const {optimisticData, finallyData} = getOnyxLoadingData(hash); API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList}, {optimisticData, finallyData}); @@ -108,4 +126,4 @@ function exportSearchItemsToCSV(query: string, reportIDList: Array Date: Thu, 18 Jul 2024 11:18:20 -0600 Subject: [PATCH 34/54] rename files --- ...RequestOnSearch.ts => ApproveMoneyRequestOnSearchParams.ts} | 0 ...oneyRequestOnSearch.ts => PayMoneyRequestOnSearchParams.ts} | 0 ...yRequestOnSearch.ts => SubmitMoneyRequestOnSearchParams.ts} | 0 src/libs/API/parameters/index.ts | 3 +++ 4 files changed, 3 insertions(+) rename src/libs/API/parameters/{ApproveMoneyRequestOnSearch.ts => ApproveMoneyRequestOnSearchParams.ts} (100%) rename src/libs/API/parameters/{PayMoneyRequestOnSearch.ts => PayMoneyRequestOnSearchParams.ts} (100%) rename src/libs/API/parameters/{SubmitMoneyRequestOnSearch.ts => SubmitMoneyRequestOnSearchParams.ts} (100%) diff --git a/src/libs/API/parameters/ApproveMoneyRequestOnSearch.ts b/src/libs/API/parameters/ApproveMoneyRequestOnSearchParams.ts similarity index 100% rename from src/libs/API/parameters/ApproveMoneyRequestOnSearch.ts rename to src/libs/API/parameters/ApproveMoneyRequestOnSearchParams.ts diff --git a/src/libs/API/parameters/PayMoneyRequestOnSearch.ts b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts similarity index 100% rename from src/libs/API/parameters/PayMoneyRequestOnSearch.ts rename to src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts diff --git a/src/libs/API/parameters/SubmitMoneyRequestOnSearch.ts b/src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts similarity index 100% rename from src/libs/API/parameters/SubmitMoneyRequestOnSearch.ts rename to src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 33381b537237..fd3a71dfbdd7 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -246,6 +246,9 @@ export type {default as UpgradeToCorporateParams} from './UpgradeToCorporatePara export type {default as DeleteMoneyRequestOnSearchParams} from './DeleteMoneyRequestOnSearchParams'; export type {default as HoldMoneyRequestOnSearchParams} from './HoldMoneyRequestOnSearchParams'; export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams'; +export type {default as SubmitMoneyRequestOnSearchParams} from './SubmitMoneyRequestOnSearchParams'; +export type {default as ApproveMoneyRequestOnSearchParams} from './ApproveMoneyRequestOnSearchParams'; +export type {default as PayMoneyRequestOnSearchParams} from './PayMoneyRequestOnSearchParams'; export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams'; export type {default as DeletePolicyReportField} from './DeletePolicyReportField'; export type {default as ConnectPolicyToNetSuiteParams} from './ConnectPolicyToNetSuiteParams'; From ad9b45292980c7a175aadcaf911007d32a008520 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 11:20:46 -0600 Subject: [PATCH 35/54] rm unused imports --- .../SelectionList/Search/ActionCell.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 5a211c9a9398..8d3f6bf7f4c0 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -8,12 +8,10 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; import * as SearchActions from '@userActions/Search'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import ROUTES from '@src/ROUTES'; import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; const actionTranslationsMap: Record = { @@ -28,7 +26,7 @@ const actionTranslationsMap: Record = type ActionCellProps = { action?: SearchTransactionAction; - transactionID?: string; + reportID?: string; isLargeScreenWidth?: boolean; isSelected?: boolean; goToItem: () => void; @@ -38,7 +36,7 @@ type ActionCellProps = { function ActionCell({ action = CONST.SEARCH.ACTION_TYPES.VIEW, - transactionID, + reportID, isLargeScreenWidth = true, isSelected = false, goToItem, @@ -53,24 +51,24 @@ function ActionCell({ const {currentSearchHash} = useSearchContext(); const onButtonPress = useCallback(() => { - if (!transactionID) { + if (!reportID) { return; } if (action === CONST.SEARCH.ACTION_TYPES.SUBMIT) { - SearchActions.submitMoneyRequestOnSearch(currentSearchHash, [transactionID]); + SearchActions.submitMoneyRequestOnSearch(currentSearchHash, [reportID]); return; } if (action === CONST.SEARCH.ACTION_TYPES.APPROVE) { - SearchActions.approveMoneyRequestOnSearch(currentSearchHash, [transactionID]); + SearchActions.approveMoneyRequestOnSearch(currentSearchHash, [reportID]); return; } if (action === CONST.SEARCH.ACTION_TYPES.PAY) { - SearchActions.payMoneyRequestOnSearch(currentSearchHash, [transactionID]); + SearchActions.payMoneyRequestOnSearch(currentSearchHash, [reportID]); } - }, [action, currentSearchHash, transactionID]); + }, [action, currentSearchHash, reportID]); const text = translate(actionTranslationsMap[action]); From 9d9a179552edcb41a9e98104915a8959adc422bc Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 11:24:59 -0600 Subject: [PATCH 36/54] use correct params for pay --- src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts | 2 +- src/libs/actions/Search.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts index 5eb49063b9fc..c3d234dfd873 100644 --- a/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts +++ b/src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts @@ -1,6 +1,6 @@ type PayMoneyRequestOnSearchParams = { hash: number; - reportIDList: string[]; + reportsAndAmounts: Record; }; export default PayMoneyRequestOnSearchParams; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 140d2e41e934..f284ee136550 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -94,10 +94,10 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[]) { API.write(WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData}); } -function payMoneyRequestOnSearch(hash: number, reportIDList: string[]) { +function payMoneyRequestOnSearch(hash: number, reportsAndAmounts: Record) { const {optimisticData, finallyData} = getOnyxLoadingData(hash); - API.write(WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData}); + API.write(WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH, {hash, reportsAndAmounts}, {optimisticData, finallyData}); } function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { From 136f04406aec16dda5f8906bf53eaf87df0635c3 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 11:28:07 -0600 Subject: [PATCH 37/54] rm pay method for now --- src/components/SelectionList/Search/ActionCell.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 8d3f6bf7f4c0..5db4337debdf 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -64,10 +64,6 @@ function ActionCell({ SearchActions.approveMoneyRequestOnSearch(currentSearchHash, [reportID]); return; } - - if (action === CONST.SEARCH.ACTION_TYPES.PAY) { - SearchActions.payMoneyRequestOnSearch(currentSearchHash, [reportID]); - } }, [action, currentSearchHash, reportID]); const text = translate(actionTranslationsMap[action]); From 1143ca460dfe0b1c6952a0ceb67f822bf4a78be3 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 11:39:58 -0600 Subject: [PATCH 38/54] handle hold bulk action --- src/components/Search/SearchPageHeader.tsx | 4 ++-- src/components/Search/types.ts | 6 ++++++ src/components/SelectionList/Search/ActionCell.tsx | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index eeb4b7a8d29f..e3722a5334ec 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -104,7 +104,7 @@ function SearchPageHeader({ }); } - const itemsToHold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].action === CONST.SEARCH.BULK_ACTION_TYPES.HOLD); + const itemsToHold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].canHold); if (itemsToHold.length > 0) { options.push({ @@ -127,7 +127,7 @@ function SearchPageHeader({ }); } - const itemsToUnhold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].action === CONST.SEARCH.BULK_ACTION_TYPES.UNHOLD); + const itemsToUnhold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].canUnhold); if (itemsToUnhold.length > 0) { options.push({ diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index 2238b0f49855..f483feba9eda 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -9,6 +9,12 @@ type SelectedTransactionInfo = { /** If the transaction can be deleted */ canDelete: boolean; + /** If the transaction can be put on hold */ + canHold: boolean; + + /** If the transaction can be removed from hold */ + canUnhold: boolean; + /** The action that can be performed for the transaction */ action: string; }; diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 5db4337debdf..2cf3562e6ccc 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -62,7 +62,6 @@ function ActionCell({ if (action === CONST.SEARCH.ACTION_TYPES.APPROVE) { SearchActions.approveMoneyRequestOnSearch(currentSearchHash, [reportID]); - return; } }, [action, currentSearchHash, reportID]); From e91932a1a1c2eb7e5d7ca3fc016ceb3b18aaf06d Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 11:41:59 -0600 Subject: [PATCH 39/54] pass hold/unhold values --- src/components/Search/SearchListWithHeader.tsx | 4 ++-- src/types/onyx/SearchResults.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchListWithHeader.tsx b/src/components/Search/SearchListWithHeader.tsx index 87a39fdd504b..bd4b843bbd60 100644 --- a/src/components/Search/SearchListWithHeader.tsx +++ b/src/components/Search/SearchListWithHeader.tsx @@ -26,7 +26,7 @@ type SearchListWithHeaderProps = Omit Date: Thu, 18 Jul 2024 11:52:23 -0600 Subject: [PATCH 40/54] reorder items --- src/components/Search/SearchPageHeader.tsx | 38 +++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index e3722a5334ec..43e4ad6f3b06 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -85,25 +85,6 @@ function SearchPageHeader({ options.push(downloadOption); } - const itemsToDelete = Object.keys(selectedTransactions ?? {}).filter((id) => selectedTransactions[id].canDelete); - - if (itemsToDelete.length > 0) { - options.push({ - icon: Expensicons.Trashcan, - text: translate('search.bulkActions.delete'), - value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE, - shouldCloseModalOnSelect: true, - onSelected: () => { - if (isOffline) { - setOfflineModalOpen?.(); - return; - } - - onSelectDeleteOption?.(itemsToDelete); - }, - }); - } - const itemsToHold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].canHold); if (itemsToHold.length > 0) { @@ -150,6 +131,25 @@ function SearchPageHeader({ }); } + const itemsToDelete = Object.keys(selectedTransactions ?? {}).filter((id) => selectedTransactions[id].canDelete); + + if (itemsToDelete.length > 0) { + options.push({ + icon: Expensicons.Trashcan, + text: translate('search.bulkActions.delete'), + value: CONST.SEARCH.BULK_ACTION_TYPES.DELETE, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setOfflineModalOpen?.(); + return; + } + + onSelectDeleteOption?.(itemsToDelete); + }, + }); + } + if (options.length === 0) { const emptyOptionStyle = { interactive: false, From 0baa059e71daf59be6d6e6451f949a0d318c6cb7 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 11:56:44 -0600 Subject: [PATCH 41/54] only show options that apply to all selected items --- src/components/Search/SearchPageHeader.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 43e4ad6f3b06..7b1355758895 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -85,9 +85,9 @@ function SearchPageHeader({ options.push(downloadOption); } - const itemsToHold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].canHold); + const shouldShowHoldOption = selectedTransactionsKeys.every((id) => selectedTransactions[id].canHold); - if (itemsToHold.length > 0) { + if (shouldShowHoldOption) { options.push({ icon: Expensicons.Stopwatch, text: translate('search.bulkActions.hold'), @@ -103,14 +103,14 @@ function SearchPageHeader({ if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } - SearchActions.holdMoneyRequestOnSearch(hash, itemsToHold, ''); + SearchActions.holdMoneyRequestOnSearch(hash, selectedTransactionsKeys, ''); }, }); } - const itemsToUnhold = selectedTransactionsKeys.filter((id) => selectedTransactions[id].canUnhold); + const shouldShowUnholdOption = selectedTransactionsKeys.every((id) => selectedTransactions[id].canUnhold); - if (itemsToUnhold.length > 0) { + if (shouldShowUnholdOption) { options.push({ icon: Expensicons.Stopwatch, text: translate('search.bulkActions.unhold'), @@ -126,14 +126,14 @@ function SearchPageHeader({ if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } - SearchActions.unholdMoneyRequestOnSearch(hash, itemsToUnhold); + SearchActions.unholdMoneyRequestOnSearch(hash, selectedTransactionsKeys); }, }); } - const itemsToDelete = Object.keys(selectedTransactions ?? {}).filter((id) => selectedTransactions[id].canDelete); + const shouldShowDeleteOption = Object.keys(selectedTransactions ?? {}).every((id) => selectedTransactions[id].canDelete); - if (itemsToDelete.length > 0) { + if (shouldShowDeleteOption) { options.push({ icon: Expensicons.Trashcan, text: translate('search.bulkActions.delete'), @@ -145,7 +145,7 @@ function SearchPageHeader({ return; } - onSelectDeleteOption?.(itemsToDelete); + onSelectDeleteOption?.(selectedTransactionsKeys); }, }); } From b816d2152451f1fd57df8280ab467647817e35a8 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 12:02:16 -0600 Subject: [PATCH 42/54] rm route params --- src/ROUTES.ts | 5 +---- src/components/Search/SearchPageHeader.tsx | 4 +++- src/libs/Navigation/linkingConfig/config.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0ccb210c9d7a..68bf629bf8f3 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -60,10 +60,7 @@ const ROUTES = { getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const, }, - TRANSACTION_HOLD_REASON_RHP: { - route: 'search/:query/hold/:transactionID', - getRoute: (query: string, transactionID: string) => `search/${query}/hold/${transactionID}` as const, - }, + TRANSACTION_HOLD_REASON_RHP: 'search/:query/hold', // This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated CONCIERGE: 'concierge', diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 7b1355758895..6cfb96e744b7 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -14,6 +14,8 @@ import * as SearchActions from '@libs/actions/Search'; import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import Navigation from '@libs/Navigation/Navigation'; import type {SearchQuery, SearchReport} from '@src/types/onyx/SearchResults'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -103,7 +105,7 @@ function SearchPageHeader({ if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } - SearchActions.holdMoneyRequestOnSearch(hash, selectedTransactionsKeys, ''); + Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP); }, }); } diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c37536dc5419..5048a053ef0a 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -965,7 +965,7 @@ const config: LinkingOptions['config'] = { [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { screens: { [SCREENS.SEARCH.REPORT_RHP]: ROUTES.SEARCH_REPORT.route, - [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP.route, + [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP, }, }, [SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: { From 4fc9c4e6944363eca52abac8765469720964b486 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 12:04:53 -0600 Subject: [PATCH 43/54] add query param back --- src/ROUTES.ts | 5 ++++- src/components/Search/SearchPageHeader.tsx | 2 +- src/libs/Navigation/linkingConfig/config.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 68bf629bf8f3..d8bba1a3615b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -60,7 +60,10 @@ const ROUTES = { getRoute: (query: string, reportID: string) => `search/${query}/view/${reportID}` as const, }, - TRANSACTION_HOLD_REASON_RHP: 'search/:query/hold', + TRANSACTION_HOLD_REASON_RHP: { + route: 'search/:query/hold', + getRoute: (query: string) => `search/${query}/hold` as const, + }, // This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated CONCIERGE: 'concierge', diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 6cfb96e744b7..c6c5ea9ff692 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -105,7 +105,7 @@ function SearchPageHeader({ if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } - Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP); + Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(query)); }, }); } diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 5048a053ef0a..c37536dc5419 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -965,7 +965,7 @@ const config: LinkingOptions['config'] = { [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { screens: { [SCREENS.SEARCH.REPORT_RHP]: ROUTES.SEARCH_REPORT.route, - [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP, + [SCREENS.SEARCH.TRANSACTION_HOLD_REASON_RHP]: ROUTES.TRANSACTION_HOLD_REASON_RHP.route, }, }, [SCREENS.RIGHT_MODAL.SEARCH_ADVANCED_FILTERS]: { From 42d64bc9feff77c794f8f4315bf25863cd99d1e3 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 12:06:08 -0600 Subject: [PATCH 44/54] rename methods --- src/components/Search/SearchContext.tsx | 8 ++++---- src/components/Search/types.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Search/SearchContext.tsx b/src/components/Search/SearchContext.tsx index 3911780d3965..367a03e081cd 100644 --- a/src/components/Search/SearchContext.tsx +++ b/src/components/Search/SearchContext.tsx @@ -6,7 +6,7 @@ const defaultSearchContext = { currentSearchHash: -1, selectedTransactionIDs: [], setCurrentSearchHash: () => {}, - setSelectedTransactionIds: () => {}, + setSelectedTransactionIDs: () => {}, }; const Context = React.createContext(defaultSearchContext); @@ -27,7 +27,7 @@ function SearchContextProvider({children}: ChildrenProps) { [searchContextData], ); - const setSelectedTransactionIds = useCallback( + const setSelectedTransactionIDs = useCallback( (selectedTransactionIDs: string[]) => { setSearchContextData({ ...searchContextData, @@ -41,9 +41,9 @@ function SearchContextProvider({children}: ChildrenProps) { () => ({ ...searchContextData, setCurrentSearchHash, - setSelectedTransactionIds, + setSelectedTransactionIDs, }), - [searchContextData, setCurrentSearchHash, setSelectedTransactionIds], + [searchContextData, setCurrentSearchHash, setSelectedTransactionIDs], ); return {children}; diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index f483feba9eda..232ae841d4ff 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -29,7 +29,7 @@ type SearchContext = { currentSearchHash: number; selectedTransactionIDs: string[]; setCurrentSearchHash: (hash: number) => void; - setSelectedTransactionIds: (selectedTransactionIds: string[]) => void; + setSelectedTransactionIDs: (selectedTransactionIds: string[]) => void; }; type ASTNode = { From 12fd3bd557c1595973f309eadbfab860f17c9bc7 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 12:15:38 -0600 Subject: [PATCH 45/54] clean up code --- src/components/Search/SearchPageHeader.tsx | 4 ++++ src/pages/Search/SearchHoldReasonPage.tsx | 7 +++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index c6c5ea9ff692..283be86001ae 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -20,6 +20,7 @@ import type {SearchQuery, SearchReport} from '@src/types/onyx/SearchResults'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type IconAsset from '@src/types/utils/IconAsset'; import getDownloadOption from './SearchActionOptionsUtils'; +import {useSearchContext} from './SearchContext'; import type {SelectedTransactions} from './types'; type SearchPageHeaderProps = { @@ -55,6 +56,8 @@ function SearchPageHeader({ const {isOffline} = useNetwork(); const {activeWorkspaceID} = useActiveWorkspace(); const {isSmallScreenWidth} = useResponsiveLayout(); + const {setSelectedTransactionIDs} = useSearchContext(); + const headerContent: {[key in SearchQuery]: {icon: IconAsset; title: string}} = { all: {icon: Illustrations.MoneyReceipts, title: translate('common.expenses')}, shared: {icon: Illustrations.SendMoney, title: translate('common.shared')}, @@ -105,6 +108,7 @@ function SearchPageHeader({ if (isMobileSelectionModeActive) { setIsMobileSelectionModeActive?.(false); } + setSelectedTransactionIDs(selectedTransactionsKeys); Navigation.navigate(ROUTES.TRANSACTION_HOLD_REASON_RHP.getRoute(query)); }, }); diff --git a/src/pages/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index 2506f6bf2453..50044e784a63 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -28,12 +28,11 @@ type SearchHoldReasonPageProps = { function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { const {translate} = useLocalize(); - const {currentSearchHash} = useSearchContext(); - const {transactionID, backTo} = route.params; + const {currentSearchHash, selectedTransactionIDs} = useSearchContext(); + const {backTo} = route.params; const onSubmit = (values: FormOnyxValues) => { - SearchActions.holdMoneyRequestOnSearch(currentSearchHash, [transactionID], values.comment); - + SearchActions.holdMoneyRequestOnSearch(currentSearchHash, selectedTransactionIDs, values.comment); Navigation.goBack(); }; From 8ae70a1a064e34770bd7b1ff8eba1743d74b55d6 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 12:17:57 -0600 Subject: [PATCH 46/54] fix lint --- src/components/Search/SearchPageHeader.tsx | 3 ++- src/libs/actions/Search.ts | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 283be86001ae..8a4f6efda21d 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -11,11 +11,11 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as SearchActions from '@libs/actions/Search'; +import Navigation from '@libs/Navigation/Navigation'; import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import Navigation from '@libs/Navigation/Navigation'; import type {SearchQuery, SearchReport} from '@src/types/onyx/SearchResults'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -194,6 +194,7 @@ function SearchPageHeader({ activeWorkspaceID, selectedReports, styles.textWrap, + setSelectedTransactionIDs, ]); if (isSmallScreenWidth) { diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index f284ee136550..df8fb27b9645 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -126,4 +126,14 @@ function exportSearchItemsToCSV(query: string, reportIDList: Array Date: Thu, 18 Jul 2024 12:19:53 -0600 Subject: [PATCH 47/54] fix button selected style --- src/components/SelectionList/Search/ActionCell.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 2cf3562e6ccc..903ac2b038e7 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -129,7 +129,6 @@ function ActionCell({ success pressOnEnter style={[styles.w100]} - innerStyles={buttonInnerStyles} /> ); } From 8cf0fc26624f9d6e1bc29c31e72ef3b4eacb30f3 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 18 Jul 2024 12:45:39 -0600 Subject: [PATCH 48/54] rm submit, approve, pay code --- src/CONST.ts | 10 ++---- .../SelectionList/Search/ActionCell.tsx | 32 ------------------- .../ApproveMoneyRequestOnSearchParams.ts | 6 ---- .../PayMoneyRequestOnSearchParams.ts | 6 ---- .../SubmitMoneyRequestOnSearchParams.ts | 6 ---- src/libs/API/parameters/index.ts | 3 -- src/libs/API/types.ts | 6 ---- src/libs/actions/Search.ts | 21 ------------ 8 files changed, 2 insertions(+), 88 deletions(-) delete mode 100644 src/libs/API/parameters/ApproveMoneyRequestOnSearchParams.ts delete mode 100644 src/libs/API/parameters/PayMoneyRequestOnSearchParams.ts delete mode 100644 src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts diff --git a/src/CONST.ts b/src/CONST.ts index 37563cb032de..f7f7e40d5043 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5190,20 +5190,14 @@ const CONST = { ACTION_TYPES: { VIEW: 'view', REVIEW: 'review', - SUBMIT: 'submit', - APPROVE: 'approve', - PAY: 'pay', DONE: 'done', PAID: 'paid', }, BULK_ACTION_TYPES: { - DELETE: 'delete', + EXPORT: 'export', HOLD: 'hold', UNHOLD: 'unhold', - SUBMIT: 'submit', - APPROVE: 'approve', - PAY: 'pay', - EXPORT: 'export', + DELETE: 'delete', }, TRANSACTION_TYPE: { CASH: 'cash', diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx index 903ac2b038e7..9cf82e4483be 100644 --- a/src/components/SelectionList/Search/ActionCell.tsx +++ b/src/components/SelectionList/Search/ActionCell.tsx @@ -17,16 +17,12 @@ import type {SearchTransactionAction} from '@src/types/onyx/SearchResults'; const actionTranslationsMap: Record = { view: 'common.view', review: 'common.review', - submit: 'common.submit', - approve: 'iou.approve', - pay: 'iou.pay', done: 'common.done', paid: 'iou.settledExpensify', }; type ActionCellProps = { action?: SearchTransactionAction; - reportID?: string; isLargeScreenWidth?: boolean; isSelected?: boolean; goToItem: () => void; @@ -36,7 +32,6 @@ type ActionCellProps = { function ActionCell({ action = CONST.SEARCH.ACTION_TYPES.VIEW, - reportID, isLargeScreenWidth = true, isSelected = false, goToItem, @@ -48,23 +43,6 @@ function ActionCell({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {currentSearchHash} = useSearchContext(); - - const onButtonPress = useCallback(() => { - if (!reportID) { - return; - } - - if (action === CONST.SEARCH.ACTION_TYPES.SUBMIT) { - SearchActions.submitMoneyRequestOnSearch(currentSearchHash, [reportID]); - return; - } - - if (action === CONST.SEARCH.ACTION_TYPES.APPROVE) { - SearchActions.approveMoneyRequestOnSearch(currentSearchHash, [reportID]); - } - }, [action, currentSearchHash, reportID]); - const text = translate(actionTranslationsMap[action]); const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID); @@ -121,16 +99,6 @@ function ActionCell({ /> ); } - return ( -