diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx index 35fa4d02f5e0..2f09b27f3067 100644 --- a/src/components/FixedFooter.tsx +++ b/src/components/FixedFooter.tsx @@ -2,6 +2,8 @@ import type {ReactNode} from 'react'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import useKeyboardState from '@hooks/useKeyboardState'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; type FixedFooterProps = { @@ -13,8 +15,17 @@ type FixedFooterProps = { }; function FixedFooter({style, children}: FixedFooterProps) { + const {isKeyboardShown} = useKeyboardState(); + const insets = useSafeAreaInsets(); const styles = useThemeStyles(); - return {children}; + + if (!children) { + return null; + } + + const shouldAddBottomPadding = isKeyboardShown || !insets.bottom; + + return {children}; } FixedFooter.displayName = 'FixedFooter'; diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 27db5687a925..8182ee487a80 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -79,7 +79,7 @@ function FormAlertWithSubmitButton({ return ( {props.children} @@ -69,4 +71,5 @@ export { useBlockedFromConcierge, useReportActionsDrafts, useSession, + useAccount, }; diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index c93b75bf11ad..0588f31a0a8c 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -1,43 +1,49 @@ -import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import React, {useEffect} from 'react'; +import type {ViewStyle} from 'react-native'; +import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as User from '@userActions/User'; import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type * as OnyxTypes from '@src/types/onyx'; import Icon from './Icon'; import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; import Text from './Text'; import Tooltip from './Tooltip'; -type ReferralProgramCTAOnyxProps = { - dismissedReferralBanners: OnyxEntry; -}; - -type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & { +type ReferralProgramCTAProps = { referralContentType: | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; + style?: ViewStyle; + onDismiss?: () => void; }; -function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: ReferralProgramCTAProps) { +function ReferralProgramCTA({referralContentType, style, onDismiss}: ReferralProgramCTAProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); + const {isDismissed, setAsDismissed} = useDismissedReferralBanners({referralContentType}); const handleDismissCallToAction = () => { - User.dismissReferralBanner(referralContentType); + setAsDismissed(); + onDismiss?.(); }; - if (!referralContentType || dismissedReferralBanners?.[referralContentType]) { + const shouldShowBanner = referralContentType && !isDismissed; + + useEffect(() => { + if (shouldShowBanner) { + return; + } + onDismiss?.(); + }, [onDismiss, shouldShowBanner]); + + if (!shouldShowBanner) { return null; } @@ -46,7 +52,7 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref onPress={() => { Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType, Navigation.getActiveRouteWithoutParams())); }} - style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]} + style={[styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5, style]} accessibilityLabel="referral" role={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -81,8 +87,4 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref ); } -export default withOnyx({ - dismissedReferralBanners: { - key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, - }, -})(ReferralProgramCTA); +export default ReferralProgramCTA; diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index eae8169de046..e53823860ce0 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -1,7 +1,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; import type {ForwardedRef, ReactNode} from 'react'; -import React, {forwardRef, useEffect, useRef, useState} from 'react'; +import React, {createContext, forwardRef, useEffect, useMemo, useRef, useState} from 'react'; import type {DimensionValue, StyleProp, ViewStyle} from 'react-native'; import {Keyboard, PanResponder, View} from 'react-native'; import {PickerAvoidingView} from 'react-native-picker-select'; @@ -99,6 +99,8 @@ type ScreenWrapperProps = { shouldShowOfflineIndicatorInWideScreen?: boolean; }; +const ScreenWrapperStatusContext = createContext({didScreenTransitionEnd: false}); + function ScreenWrapper( { shouldEnableMaxHeight = false, @@ -201,6 +203,7 @@ function ScreenWrapper( }, []); const isAvoidingViewportScroll = useTackInputFocus(shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileSafari()); + const contextValue = useMemo(() => ({didScreenTransitionEnd}), [didScreenTransitionEnd]); return ( @@ -251,16 +254,18 @@ function ScreenWrapper( {isDevelopment && } - { - // If props.children is a function, call it to provide the insets to the children. - typeof children === 'function' - ? children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd, - }) - : children - } + + { + // If props.children is a function, call it to provide the insets to the children. + typeof children === 'function' + ? children({ + insets, + safeAreaPaddingBottomStyle, + didScreenTransitionEnd, + }) + : children + } + {isSmallScreenWidth && shouldShowOfflineIndicator && } {!isSmallScreenWidth && shouldShowOfflineIndicatorInWideScreen && ( ( showConfirmButton = false, shouldPreventDefaultFocusOnSelectRow = false, containerStyle, - isKeyboardShown = false, disableKeyboardShortcuts = false, children, shouldStopPropagation = false, @@ -88,6 +88,7 @@ function BaseSelectionList( const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); const [isInitialSectionListRender, setIsInitialSectionListRender] = useState(true); + const {isKeyboardShown} = useKeyboardState(); const [itemsToHighlight, setItemsToHighlight] = useState | null>(null); const itemFocusTimeoutRef = useRef(null); const [currentPage, setCurrentPage] = useState(1); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 38c5f03fcae6..af2ea3469408 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -284,8 +284,8 @@ type BaseSelectionListProps = Partial & { /** Styles to apply to SelectionList container */ containerStyle?: StyleProp; - /** Whether keyboard is visible on the screen */ - isKeyboardShown?: boolean; + /** Whether focus event should be delayed */ + shouldDelayFocus?: boolean; /** Component to display on the right side of each child */ rightHandSideComponent?: ((item: ListItem) => ReactElement | null) | ReactElement | null; diff --git a/src/hooks/useDismissedReferralBanners.ts b/src/hooks/useDismissedReferralBanners.ts new file mode 100644 index 000000000000..94ccd0a0b567 --- /dev/null +++ b/src/hooks/useDismissedReferralBanners.ts @@ -0,0 +1,29 @@ +import {useOnyx} from 'react-native-onyx'; +import * as User from '@userActions/User'; +import type CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type UseDismissedReferralBannersProps = { + referralContentType: + | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST + | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT + | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY + | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; +}; + +function useDismissedReferralBanners({referralContentType}: UseDismissedReferralBannersProps): {isDismissed: boolean; setAsDismissed: () => void} { + const [dismissedReferralBanners] = useOnyx(ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS); + const isDismissed = dismissedReferralBanners?.[referralContentType] ?? false; + + const setAsDismissed = () => { + if (!referralContentType) { + return; + } + // Set the banner as dismissed + User.dismissReferralBanner(referralContentType); + }; + + return {isDismissed, setAsDismissed}; +} + +export default useDismissedReferralBanners; diff --git a/src/hooks/useScreenWrapperTransitionStatus.ts b/src/hooks/useScreenWrapperTransitionStatus.ts new file mode 100644 index 000000000000..b9e94abfc024 --- /dev/null +++ b/src/hooks/useScreenWrapperTransitionStatus.ts @@ -0,0 +1,17 @@ +import {useContext} from 'react'; +import {ScreenWrapperStatusContext} from '@components/ScreenWrapper'; + +/** + * Hook to get the transition status of a screen inside a ScreenWrapper. + * Use this hook if you can't get the transition status from the ScreenWrapper itself. Usually when ScreenWrapper is used inside TopTabNavigator. + * @returns `didScreenTransitionEnd` flag to indicate if navigation transition ended. + */ +export default function useScreenWrapperTranstionStatus() { + const value = useContext(ScreenWrapperStatusContext); + + if (value === undefined) { + throw new Error("Couldn't find values for screen ScreenWrapper transition status. Are you inside a screen in ScreenWrapper?"); + } + + return value; +} diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 49e53381e040..ab5bd10317be 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -192,6 +192,7 @@ function RoomInvitePage({betas, report, policies}: RoomInvitePageProps) { - - + ); } diff --git a/src/pages/SearchPage/index.tsx b/src/pages/SearchPage/index.tsx index 7d2a5bfecbb8..5576f64ba67a 100644 --- a/src/pages/SearchPage/index.tsx +++ b/src/pages/SearchPage/index.tsx @@ -1,7 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import isEmpty from 'lodash/isEmpty'; import React, {useEffect, useMemo, useState} from 'react'; -import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -10,6 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; import useDebouncedState from '@hooks/useDebouncedState'; +import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -37,8 +37,6 @@ type SearchPageOnyxProps = { type SearchPageProps = SearchPageOnyxProps & StackScreenProps; -type Options = OptionsListUtils.Options & {headerMessage: string}; - type SearchPageSectionItem = { data: OptionData[]; shouldShow: boolean; @@ -51,7 +49,7 @@ const setPerformanceTimersEnd = () => { Performance.markEnd(CONST.TIMING.SEARCH_RENDER); }; -const SearchPageFooterInstance = ; +const SerachPageFooterInstance = ; function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) { const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); @@ -75,8 +73,8 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) Report.searchInServer(debouncedSearchValue.trim()); }, [debouncedSearchValue]); - const searchOptions: Options = useMemo(() => { - if (!areOptionsInitialized) { + const searchOptions = useMemo(() => { + if (!areOptionsInitialized || !isScreenTransitionEnd) { return { recentReports: [], personalDetails: [], @@ -91,7 +89,7 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, Boolean(optionList.userToInvite), ''); return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, betas, options]); + }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]); const filteredOptions = useMemo(() => { if (debouncedSearchValue.trim() === '') { @@ -159,6 +157,8 @@ function SearchPage({betas, isSearchingForReports, navigation}: SearchPageProps) setIsScreenTransitionEnd(true); }; + const {isDismissed} = useDismissedReferralBanners({referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}); + return ( - {({safeAreaPaddingBottomStyle}) => ( - <> - - - - sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} - ListItem={UserListItem} - textInputValue={searchValue} - textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} - textInputHint={offlineMessage} - onChangeText={setSearchValue} - headerMessage={headerMessage} - headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined} - onLayout={setPerformanceTimersEnd} - onSelectRow={selectReport} - showLoadingPlaceholder={!areOptionsInitialized} - footerContent={SearchPageFooterInstance} - isLoadingNewOptions={isSearchingForReports ?? undefined} - /> - - - )} + + + sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} + ListItem={UserListItem} + textInputValue={searchValue} + textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} + textInputHint={offlineMessage} + onChangeText={setSearchValue} + headerMessage={headerMessage} + headerMessageStyle={headerMessage === translate('common.noResultsFound') ? [themeStyles.ph4, themeStyles.pb5] : undefined} + onLayout={setPerformanceTimersEnd} + onSelectRow={selectReport} + showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd} + footerContent={!isDismissed && SerachPageFooterInstance} + isLoadingNewOptions={isSearchingForReports ?? undefined} + /> ); } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 4bac6d8c018c..3c65f0fa9a96 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -1,7 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {memo, useCallback, useEffect, useMemo} from 'react'; -import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Button from '@components/Button'; @@ -14,9 +13,11 @@ import SelectCircle from '@components/SelectCircle'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; import useDebouncedState from '@hooks/useDebouncedState'; +import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -28,9 +29,6 @@ const propTypes = { /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string), - /** An object that holds data about which referral banners have been dismissed */ - dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool), - /** Callback to request parent modal to go to next step, which should be split */ onFinish: PropTypes.func.isRequired, @@ -48,45 +46,28 @@ const propTypes = { }), ), - /** Padding bottom style of safe area */ - safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - /** The type of IOU report, i.e. bill, request, send */ iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)).isRequired, /** The request type, ie. manual, scan, distance */ iouRequestType: PropTypes.oneOf(_.values(CONST.IOU.REQUEST_TYPE)).isRequired, - - /** Whether the parent screen transition has ended */ - didScreenTransitionEnd: PropTypes.bool, }; const defaultProps = { participants: [], - safeAreaPaddingBottomStyle: {}, betas: [], - dismissedReferralBanners: {}, - didScreenTransitionEnd: false, }; -function MoneyTemporaryForRefactorRequestParticipantsSelector({ - betas, - participants, - onFinish, - onParticipantsAdded, - safeAreaPaddingBottomStyle, - iouType, - iouRequestType, - dismissedReferralBanners, - didScreenTransitionEnd, -}) { +function MoneyTemporaryForRefactorRequestParticipantsSelector({betas, participants, onFinish, onParticipantsAdded, iouType, iouRequestType}) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); - const {canUseP2PDistanceRequests} = usePermissions(iouType); + const {isDismissed} = useDismissedReferralBanners({referralContentType}); + const {canUseP2PDistanceRequests} = usePermissions(); + const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); const {options, areOptionsInitialized} = useOptionsList({ shouldInitialize: didScreenTransitionEnd, }); @@ -106,7 +87,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ */ const [sections, newChatOptions] = useMemo(() => { const newSections = []; - if (!areOptionsInitialized) { + if (!areOptionsInitialized || !didScreenTransitionEnd) { return [newSections, {}]; } const chatOptions = OptionsListUtils.getFilteredOptions( @@ -185,6 +166,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ maxParticipantsReached, personalDetails, translate, + didScreenTransitionEnd, ]); /** @@ -289,13 +271,18 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ [shouldShowSplitBillErrorMessage, onFinish, addSingleParticipant, participants], ); - const footerContent = useMemo( - () => ( - - {!dismissedReferralBanners[referralContentType] && ( - - - + const footerContent = useMemo(() => { + if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) { + return; + } + + return ( + <> + {!isDismissed && ( + )} {shouldShowSplitBillErrorMessage && ( @@ -316,10 +303,9 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ isDisabled={shouldShowSplitBillErrorMessage} /> )} - - ), - [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], - ); + + ); + }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate]); const itemRightSideComponent = useCallback( (item) => { @@ -356,23 +342,21 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ ); return ( - 0 ? safeAreaPaddingBottomStyle : {}]}> - - + ); } @@ -381,12 +365,6 @@ MoneyTemporaryForRefactorRequestParticipantsSelector.defaultProps = defaultProps MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTemporaryForRefactorRequestParticipantsSelector'; export default withOnyx({ - dismissedReferralBanners: { - key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, - }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, betas: { key: ONYXKEYS.BETAS, }, @@ -395,8 +373,6 @@ export default withOnyx({ MoneyTemporaryForRefactorRequestParticipantsSelector, (prevProps, nextProps) => _.isEqual(prevProps.participants, nextProps.participants) && - prevProps.didScreenTransitionEnd === nextProps.didScreenTransitionEnd && - _.isEqual(prevProps.dismissedReferralBanners, nextProps.dismissedReferralBanners) && prevProps.iouRequestType === nextProps.iouRequestType && prevProps.iouType === nextProps.iouType && _.isEqual(prevProps.betas, nextProps.betas), diff --git a/src/pages/iou/request/step/IOURequestStepCurrency.js b/src/pages/iou/request/step/IOURequestStepCurrency.js index 51dba5858cb5..24602d527389 100644 --- a/src/pages/iou/request/step/IOURequestStepCurrency.js +++ b/src/pages/iou/request/step/IOURequestStepCurrency.js @@ -129,6 +129,7 @@ function IOURequestStepCurrency({ onEntryTransitionEnd={() => optionsSelectorRef.current && optionsSelectorRef.current.focus()} shouldShowWrapper testID={IOURequestStepCurrency.displayName} + includeSafeAreaPaddingBottom={false} > {({didScreenTransitionEnd}) => ( {({didScreenTransitionEnd}) => ( lodashGet(route, 'params.iouType', '')); @@ -128,8 +125,8 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { shouldEnableMaxHeight={DeviceCapabilities.canUseTouchScreen()} testID={MoneyRequestParticipantsPage.displayName} > - {({safeAreaPaddingBottomStyle}) => ( - + {({didScreenTransitionEnd}) => ( + <> navigateToConfirmationStep(iouType)} navigateToSplit={() => navigateToConfirmationStep(CONST.IOU.TYPE.SPLIT)} - safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} iouType={iouType} isDistanceRequest={isDistanceRequest} isScanRequest={isScanRequest} + didScreenTransitionEnd={didScreenTransitionEnd} /> - + )} ); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 57b86bebcde5..dbde94b60e96 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -13,6 +13,8 @@ import ReferralProgramCTA from '@components/ReferralProgramCTA'; import SelectCircle from '@components/SelectCircle'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; @@ -36,9 +38,6 @@ const propTypes = { /** Callback to add participants in MoneyRequestModal */ onAddParticipants: PropTypes.func.isRequired, - /** An object that holds data about which referral banners have been dismissed */ - dismissedReferralBanners: PropTypes.objectOf(PropTypes.bool), - /** Selected participants from MoneyRequestModal with login */ participants: PropTypes.arrayOf( PropTypes.shape({ @@ -50,43 +49,32 @@ const propTypes = { }), ), - /** padding bottom style of safe area */ - safeAreaPaddingBottomStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - /** The type of IOU report, i.e. bill, request, send */ iouType: PropTypes.string.isRequired, /** Whether the money request is a distance request or not */ isDistanceRequest: PropTypes.bool, + + /** Whether the screen transition has ended */ + didScreenTransitionEnd: PropTypes.bool, }; const defaultProps = { - dismissedReferralBanners: {}, participants: [], - safeAreaPaddingBottomStyle: {}, betas: [], isDistanceRequest: false, + didScreenTransitionEnd: false, }; -function MoneyRequestParticipantsSelector({ - betas, - dismissedReferralBanners, - participants, - navigateToRequest, - navigateToSplit, - onAddParticipants, - safeAreaPaddingBottomStyle, - iouType, - isDistanceRequest, -}) { +function MoneyRequestParticipantsSelector({betas, participants, navigateToRequest, navigateToSplit, onAddParticipants, iouType, isDistanceRequest, didScreenTransitionEnd}) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); + const {options, areOptionsInitialized} = useOptionsList({shouldInitialize: didScreenTransitionEnd}); const {canUseP2PDistanceRequests} = usePermissions(iouType); - const {options, areOptionsInitialized} = useOptionsList(); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -98,7 +86,7 @@ function MoneyRequestParticipantsSelector({ options.reports, options.personalDetails, betas, - searchTerm, + debouncedSearchTerm, participants, CONST.EXPENSIFY_EMAILS, @@ -123,7 +111,7 @@ function MoneyRequestParticipantsSelector({ personalDetails: chatOptions.personalDetails, userToInvite: chatOptions.userToInvite, }; - }, [options.reports, options.personalDetails, betas, searchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]); + }, [options.reports, options.personalDetails, betas, debouncedSearchTerm, participants, iouType, canUseP2PDistanceRequests, isDistanceRequest]); /** * Returns the sections needed for the OptionsSelector @@ -134,7 +122,7 @@ function MoneyRequestParticipantsSelector({ const newSections = []; const formatResults = OptionsListUtils.formatSectionsFromSearchTerm( - searchTerm, + debouncedSearchTerm, participants, newChatOptions.recentReports, newChatOptions.personalDetails, @@ -172,7 +160,7 @@ function MoneyRequestParticipantsSelector({ } return newSections; - }, [maxParticipantsReached, newChatOptions.personalDetails, newChatOptions.recentReports, newChatOptions.userToInvite, participants, personalDetails, searchTerm, translate]); + }, [maxParticipantsReached, newChatOptions.personalDetails, newChatOptions.recentReports, newChatOptions.userToInvite, participants, personalDetails, debouncedSearchTerm, translate]); /** * Adds a single participant to the request @@ -247,11 +235,11 @@ function MoneyRequestParticipantsSelector({ OptionsListUtils.getHeaderMessage( _.get(newChatOptions, 'personalDetails', []).length + _.get(newChatOptions, 'recentReports', []).length !== 0, Boolean(newChatOptions.userToInvite), - searchTerm.trim(), + debouncedSearchTerm.trim(), maxParticipantsReached, - _.some(participants, (participant) => participant.searchText.toLowerCase().includes(searchTerm.trim().toLowerCase())), + _.some(participants, (participant) => participant.searchText.toLowerCase().includes(debouncedSearchTerm.trim().toLowerCase())), ), - [maxParticipantsReached, newChatOptions, participants, searchTerm], + [maxParticipantsReached, newChatOptions, participants, debouncedSearchTerm], ); // Right now you can't split a request with a workspace and other additional participants @@ -281,13 +269,19 @@ function MoneyRequestParticipantsSelector({ [shouldShowSplitBillErrorMessage, navigateToSplit, addSingleParticipant, participants.length], ); - const footerContent = useMemo( - () => ( + const {isDismissed} = useDismissedReferralBanners({referralContentType}); + + const footerContent = useMemo(() => { + if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) { + return null; + } + return ( - {!dismissedReferralBanners[referralContentType] && ( - - - + {!isDismissed && ( + )} {shouldShowSplitBillErrorMessage && ( @@ -309,9 +303,8 @@ function MoneyRequestParticipantsSelector({ /> )} - ), - [handleConfirmSelection, participants.length, dismissedReferralBanners, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], - ); + ); + }, [handleConfirmSelection, participants.length, isDismissed, referralContentType, shouldShowSplitBillErrorMessage, styles, translate]); const itemRightSideComponent = useCallback( (item) => { @@ -345,23 +338,21 @@ function MoneyRequestParticipantsSelector({ ); return ( - 0 ? safeAreaPaddingBottomStyle : {}]}> - - + ); } @@ -370,9 +361,6 @@ MoneyRequestParticipantsSelector.displayName = 'MoneyRequestParticipantsSelector MoneyRequestParticipantsSelector.defaultProps = defaultProps; export default withOnyx({ - dismissedReferralBanners: { - key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, - }, betas: { key: ONYXKEYS.BETAS, }, diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 3f95c3e02a5b..4a85e01d973a 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,7 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; -import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -64,7 +63,6 @@ function WorkspaceInvitePage({ invitedEmailsToAccountIDsDraft, policy, isLoadingReportData = true, - didScreenTransitionEnd, }: WorkspaceInvitePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -72,6 +70,7 @@ function WorkspaceInvitePage({ const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetailsProp); Policy.openWorkspaceInvitePage(route.params.policyID, Object.keys(policyMemberEmailsToAccountIDs)); @@ -223,18 +222,16 @@ function WorkspaceInvitePage({ setSelectedOptions(newSelectedOptions); }; - const validate = (): boolean => { + const inviteUser = useCallback(() => { const errors: Errors = {}; if (selectedOptions.length <= 0) { errors.noUserSelected = 'true'; } Policy.setWorkspaceErrors(route.params.policyID, errors); - return isEmptyObject(errors); - }; + const isValid = isEmptyObject(errors); - const inviteUser = () => { - if (!validate()) { + if (!isValid) { return; } @@ -249,7 +246,7 @@ function WorkspaceInvitePage({ }); Policy.setWorkspaceInviteMembersDraft(route.params.policyID, invitedEmailsToAccountIDs); Navigation.navigate(ROUTES.WORKSPACE_INVITE_MESSAGE.getRoute(route.params.policyID)); - }; + }, [route.params.policyID, selectedOptions]); const [policyName, shouldShowAlertPrompt] = useMemo(() => [policy?.name ?? '', !isEmptyObject(policy?.errors) || !!policy?.alertMessage], [policy]); @@ -271,11 +268,29 @@ function WorkspaceInvitePage({ return OptionsListUtils.getHeaderMessage(personalDetails.length !== 0, usersToInvite.length > 0, searchValue); }, [excludedUsers, translate, searchTerm, policyName, usersToInvite, personalDetails.length]); + const footerContent = useMemo( + () => ( + + ), + [inviteUser, policy?.alertMessage, selectedOptions.length, shouldShowAlertPrompt, styles, translate], + ); + return ( setDidScreenTransitionEnd(true)} > - - - ); diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts index 98ce460a7669..5b9470c6ca6f 100644 --- a/src/types/onyx/Account.ts +++ b/src/types/onyx/Account.ts @@ -1,5 +1,6 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; +import type DismissedReferralBanners from './DismissedReferralBanners'; import type * as OnyxCommon from './OnyxCommon'; type TwoFactorAuthStep = ValueOf | ''; @@ -60,6 +61,7 @@ type Account = { success?: string; codesAreCopied?: boolean; twoFactorAuthStep?: TwoFactorAuthStep; + dismissedReferralBanners?: DismissedReferralBanners; }; export default Account; diff --git a/tests/perf-test/SearchPage.perf-test.tsx b/tests/perf-test/SearchPage.perf-test.tsx index ea759a1201b2..33ee900f8b6c 100644 --- a/tests/perf-test/SearchPage.perf-test.tsx +++ b/tests/perf-test/SearchPage.perf-test.tsx @@ -9,6 +9,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {measurePerformance} from 'reassure'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider'; +import {KeyboardStateProvider} from '@components/withKeyboardState'; import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; import type {RootStackParamList} from '@libs/Navigation/types'; import {createOptionList} from '@libs/OptionsListUtils'; @@ -75,6 +76,15 @@ jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType return WithNavigationFocus; }); +// mock of useDismissedReferralBanners +jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: jest.fn(() => ({ + isDismissed: false, + setAsDismissed: () => {}, + })), +})); const getMockedReports = (length = 100) => createCollection( @@ -124,7 +134,7 @@ type SearchPageProps = StackScreenProps + ({ createNavigationContainerRef: jest.fn(), })); +jest.mock('../../src/hooks/useKeyboardState', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: jest.fn(() => ({ + isKeyboardShown: false, + keyboardHeight: 0, + })), +})); + function SelectionListWrapper({canSelectMultiple}: SelectionListWrapperProps) { const [selectedIds, setSelectedIds] = useState([]);