diff --git a/android/app/build.gradle b/android/app/build.gradle index 1aa439570ba6..dc3c583961c5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009006805 - versionName "9.0.68-5" + versionCode 1009006901 + versionName "9.0.69-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index e53be6f6b269..2cfd24f13ab7 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -320,14 +320,17 @@ An example of this can be seen in the [ACHContractStep](https://github.com/Expen ### Safe Area Padding -Any `FormProvider.js` that has a button will also add safe area padding by default. If the `` is inside a ``, we will want to disable the default safe area padding applied there e.g. +Any `FormProvider.tsx` that has a button at the bottom. If the `` is inside a ``, the bottom safe area inset is handled automatically (`includeSafeAreaPaddingBottom` needs to be set to `true`, but its the default). +If you have custom requirements and can't use ``, you can use the `useStyledSafeAreaInsets()` hook: ```jsx - +const { paddingTop, paddingBottom, safeAreaPaddingBottomStyle } = useStyledSafeAreaInsets(); + + {...} - + ``` ### Handling nested Pickers in Form diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 3e54fd232ce0..c2799491a67b 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.68 + 9.0.69 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.68.5 + 9.0.69.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 332986865533..aa903e8ddf1e 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.68 + 9.0.69 CFBundleSignature ???? CFBundleVersion - 9.0.68.5 + 9.0.69.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 7f10ae6b0373..e10faccd2add 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.68 + 9.0.69 CFBundleVersion - 9.0.68.5 + 9.0.69.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 763076b0c0b1..c8e92768eb9a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1766,7 +1766,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-pager-view (6.5.0): + - react-native-pager-view (6.5.1): - DoubleConversion - glog - hermes-engine @@ -1779,7 +1779,7 @@ PODS: - React-featureflags - React-graphics - React-ImageManager - - react-native-pager-view/common (= 6.5.0) + - react-native-pager-view/common (= 6.5.1) - React-NativeModulesApple - React-RCTFabric - React-rendererdebug @@ -1788,7 +1788,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-pager-view/common (6.5.0): + - react-native-pager-view/common (6.5.1): - DoubleConversion - glog - hermes-engine @@ -3224,7 +3224,7 @@ SPEC CHECKSUMS: react-native-keyboard-controller: 97bb7b48fa427c7455afdc8870c2978efd9bfa3a react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: fb5112b1fa754975485884ae85a3fb6a684f49d5 - react-native-pager-view: c64a744211a46202619a77509f802765d1659dba + react-native-pager-view: abc5ef92699233eb726442c7f452cac82f73d0cb react-native-pdf: dd6ae39a93607a80919bef9f3499e840c693989d react-native-performance: 3c608307be10964f8a97d3af462f37125b6d8fa5 react-native-plaid-link-sdk: f91a22b45b7c3d4cd6c47273200dc57df35068b0 diff --git a/package-lock.json b/package-lock.json index 16da55461f24..93ab0290c857 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.68-5", + "version": "9.0.69-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.68-5", + "version": "9.0.69-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -96,7 +96,7 @@ "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", "react-native-onyx": "2.0.82", - "react-native-pager-view": "6.5.0", + "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.10.0", @@ -33592,9 +33592,9 @@ } }, "node_modules/react-native-pager-view": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.5.0.tgz", - "integrity": "sha512-Buqc5mjCgIem7aIQU/seMKqhQr98YvBqRNilnoBb8hNGhCaQTE2yvYDwUhOytowyOkjCstLv7Fap2jcLm/k3Bw==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.5.1.tgz", + "integrity": "sha512-YdX7bP+rPYvATMU7HzlMq9JaG3ui/+cVRbFZFGW+QshDULANFg9ECR1BA7H7JTIcO/ZgWCwF+1aVmYG5yBA9Og==", "license": "MIT", "peerDependencies": { "react": "*", diff --git a/package.json b/package.json index 4d13273e4267..b27b788d04f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.68-5", + "version": "9.0.69-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -153,7 +153,7 @@ "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", "react-native-onyx": "2.0.82", - "react-native-pager-view": "6.5.0", + "react-native-pager-view": "6.5.1", "react-native-pdf": "6.7.3", "react-native-performance": "^5.1.0", "react-native-permissions": "^3.10.0", diff --git a/patches/react-native-pager-view+6.5.0.patch b/patches/react-native-pager-view+6.5.1+001+initial.patch similarity index 100% rename from patches/react-native-pager-view+6.5.0.patch rename to patches/react-native-pager-view+6.5.1+001+initial.patch diff --git a/patches/react-native-screens+3.34.0+003+fabric-flat-list-fix.patch b/patches/react-native-screens+3.34.0+003+fabric-flat-list-fix.patch new file mode 100644 index 000000000000..3327ed477893 --- /dev/null +++ b/patches/react-native-screens+3.34.0+003+fabric-flat-list-fix.patch @@ -0,0 +1,57 @@ +diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt +index 9d08d39..146b9c2 100644 +--- a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt ++++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/Screen.kt +@@ -18,6 +18,7 @@ import com.facebook.react.uimanager.PixelUtil + import com.facebook.react.uimanager.UIManagerHelper + import com.facebook.react.uimanager.UIManagerModule + import com.swmansion.rnscreens.events.HeaderHeightChangeEvent ++import com.swmansion.rnscreens.ext.isInsideScrollViewWithRemoveClippedSubviews + + @SuppressLint("ViewConstructor") // Only we construct this view, it is never inflated. + class Screen( +@@ -310,6 +311,16 @@ class Screen( + startTransitionRecursive(child.toolbar) + } + if (child is ViewGroup) { ++ // a combination of https://github.com/software-mansion/react-native-screens/pull/2307/files and https://github.com/software-mansion/react-native-screens/pull/2383/files ++ // The children are miscounted when there's a FlatList with ++ // removeClippedSubviews set to true (default). ++ // We add a simple view for each item in the list to make it work as expected. ++ // See https://github.com/software-mansion/react-native-screens/issues/2282 ++ if (child.isInsideScrollViewWithRemoveClippedSubviews()) { ++ for (j in 0 until child.childCount) { ++ child.addView(View(context)) ++ } ++ } + startTransitionRecursive(child) + } + } +diff --git a/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ext/ViewExt.kt b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ext/ViewExt.kt +new file mode 100644 +index 0000000..9d9fbfd +--- /dev/null ++++ b/node_modules/react-native-screens/android/src/main/java/com/swmansion/rnscreens/ext/ViewExt.kt +@@ -0,0 +1,21 @@ ++package com.swmansion.rnscreens.ext ++ ++import android.view.View ++import android.view.ViewGroup ++import com.facebook.react.views.scroll.ReactHorizontalScrollView ++import com.facebook.react.views.scroll.ReactScrollView ++import com.swmansion.rnscreens.ScreenStack ++ ++internal fun View.isInsideScrollViewWithRemoveClippedSubviews(): Boolean { ++ if (this is ReactHorizontalScrollView || this is ReactScrollView) { ++ return false ++ } ++ var parentView = this.parent ++ while (parentView is ViewGroup && parentView !is ScreenStack) { ++ if (parentView is ReactScrollView) { ++ return parentView.removeClippedSubviews ++ } ++ parentView = parentView.parent ++ } ++ return false ++} +\ No newline at end of file diff --git a/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch b/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch new file mode 100644 index 000000000000..62cbf68f458d --- /dev/null +++ b/patches/react-native-screens+3.34.0+004+ios-custom-animations-native-transitions.patch @@ -0,0 +1,156 @@ +diff --git a/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm b/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm +index abb2cf6..fb81d52 100644 +--- a/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm ++++ b/node_modules/react-native-screens/ios/RNSScreenStackAnimator.mm +@@ -5,13 +5,14 @@ + + // proportions to default transition duration + static const float RNSSlideOpenTransitionDurationProportion = 1; +-static const float RNSFadeOpenTransitionDurationProportion = 0.2 / 0.35; +-static const float RNSSlideCloseTransitionDurationProportion = 0.25 / 0.35; +-static const float RNSFadeCloseTransitionDurationProportion = 0.15 / 0.35; +-static const float RNSFadeCloseDelayTransitionDurationProportion = 0.1 / 0.35; ++static const float RNSFadeOpenTransitionDurationProportion = 0.2 / 0.5; ++static const float RNSSlideCloseTransitionDurationProportion = 0.25 / 0.5; ++static const float RNSFadeCloseTransitionDurationProportion = 0.15 / 0.5; ++static const float RNSFadeCloseDelayTransitionDurationProportion = 0.1 / 0.5; + // same value is used in other projects using similar approach for transistions + // and it looks the most similar to the value used by Apple + static constexpr float RNSShadowViewMaxAlpha = 0.1; ++static const int UIViewAnimationOptionCurveDefaultTransition = 7 << 16; + + @implementation RNSScreenStackAnimator { + UINavigationControllerOperation _operation; +@@ -22,7 +23,7 @@ - (instancetype)initWithOperation:(UINavigationControllerOperation)operation + { + if (self = [super init]) { + _operation = operation; +- _transitionDuration = 0.35; // default duration in seconds ++ _transitionDuration = 0.5; // default duration in seconds + } + return self; + } +@@ -129,6 +130,8 @@ - (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled + } + + [UIView animateWithDuration:[self transitionDuration:transitionContext] ++ delay:0 ++ options:UIViewAnimationOptionCurveDefaultTransition + animations:^{ + fromViewController.view.transform = leftTransform; + toViewController.view.transform = CGAffineTransformIdentity; +@@ -170,6 +173,8 @@ - (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled + + if (!transitionContext.isInteractive) { + [UIView animateWithDuration:[self transitionDuration:transitionContext] ++ delay:0 ++ options:UIViewAnimationOptionCurveDefaultTransition + animations:animationBlock + completion:completionBlock]; + } else { +@@ -203,6 +208,8 @@ - (void)animateSlideFromLeftWithTransitionContext:(id; + stackAnimation?: WithDefault; +- transitionDuration?: WithDefault; ++ transitionDuration?: WithDefault; + replaceAnimation?: WithDefault; + swipeDirection?: WithDefault; + hideKeyboardOnSwipe?: boolean; +diff --git a/node_modules/react-native-screens/lib/typescript/fabric/ScreenNativeComponent.d.ts b/node_modules/react-native-screens/lib/typescript/fabric/ScreenNativeComponent.d.ts +index 11ed190..f676e08 100644 +--- a/node_modules/react-native-screens/lib/typescript/fabric/ScreenNativeComponent.d.ts ++++ b/node_modules/react-native-screens/lib/typescript/fabric/ScreenNativeComponent.d.ts +@@ -55,7 +55,7 @@ export interface NativeProps extends ViewProps { + gestureResponseDistance?: GestureResponseDistanceType; + stackPresentation?: WithDefault; + stackAnimation?: WithDefault; +- transitionDuration?: WithDefault; ++ transitionDuration?: WithDefault; + replaceAnimation?: WithDefault; + swipeDirection?: WithDefault; + hideKeyboardOnSwipe?: boolean; +diff --git a/node_modules/react-native-screens/src/fabric/ModalScreenNativeComponent.ts b/node_modules/react-native-screens/src/fabric/ModalScreenNativeComponent.ts +index bb59c4c..d4c14ee 100644 +--- a/node_modules/react-native-screens/src/fabric/ModalScreenNativeComponent.ts ++++ b/node_modules/react-native-screens/src/fabric/ModalScreenNativeComponent.ts +@@ -90,7 +90,7 @@ export interface NativeProps extends ViewProps { + gestureResponseDistance?: GestureResponseDistanceType; + stackPresentation?: WithDefault; + stackAnimation?: WithDefault; +- transitionDuration?: WithDefault; ++ transitionDuration?: WithDefault; + replaceAnimation?: WithDefault; + swipeDirection?: WithDefault; + hideKeyboardOnSwipe?: boolean; +diff --git a/node_modules/react-native-screens/src/fabric/ScreenNativeComponent.ts b/node_modules/react-native-screens/src/fabric/ScreenNativeComponent.ts +index 4e39336..ab0b313 100644 +--- a/node_modules/react-native-screens/src/fabric/ScreenNativeComponent.ts ++++ b/node_modules/react-native-screens/src/fabric/ScreenNativeComponent.ts +@@ -92,7 +92,7 @@ export interface NativeProps extends ViewProps { + gestureResponseDistance?: GestureResponseDistanceType; + stackPresentation?: WithDefault; + stackAnimation?: WithDefault; +- transitionDuration?: WithDefault; ++ transitionDuration?: WithDefault; + replaceAnimation?: WithDefault; + swipeDirection?: WithDefault; + hideKeyboardOnSwipe?: boolean; \ No newline at end of file diff --git a/patches/react-native-screens+3.34.0+005+fix-screen-flicker-on-modal-unmount.patch b/patches/react-native-screens+3.34.0+005+fix-screen-flicker-on-modal-unmount.patch new file mode 100644 index 000000000000..bbeedbc57873 --- /dev/null +++ b/patches/react-native-screens+3.34.0+005+fix-screen-flicker-on-modal-unmount.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/react-native-screens/ios/RNSScreenStack.mm b/node_modules/react-native-screens/ios/RNSScreenStack.mm +index ea27b03..8f1d005 100644 +--- a/node_modules/react-native-screens/ios/RNSScreenStack.mm ++++ b/node_modules/react-native-screens/ios/RNSScreenStack.mm +@@ -1121,16 +1121,7 @@ - (void)mountChildComponentView:(UIView *)childCompone + - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index + { + RNSScreenView *screenChildComponent = (RNSScreenView *)childComponentView; +- +- // We should only do a snapshot of a screen that is on the top. +- // We also check `_presentedModals` since if you push 2 modals, second one is not a "child" of _controller. +- // Also, when dissmised with a gesture, the screen already is not under the window, so we don't need to apply +- // snapshot. +- if (screenChildComponent.window != nil && +- ((screenChildComponent == _controller.visibleViewController.view && _presentedModals.count < 2) || +- screenChildComponent == [_presentedModals.lastObject view])) { +- [screenChildComponent.controller setViewToSnapshot]; +- } ++ [screenChildComponent.controller setViewToSnapshot]; + + RCTAssert( + screenChildComponent.reactSuperview == self, diff --git a/src/App.tsx b/src/App.tsx index 52904e0a06c4..643e2146e501 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,6 +2,7 @@ import {PortalProvider} from '@gorhom/portal'; import React from 'react'; import {LogBox} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; +import {KeyboardProvider} from 'react-native-keyboard-controller'; import {PickerStateProvider} from 'react-native-picker-select'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import '../wdyr'; @@ -14,7 +15,6 @@ import CustomStatusBarAndBackgroundContextProvider from './components/CustomStat import ErrorBoundary from './components/ErrorBoundary'; import HTMLEngineProvider from './components/HTMLEngineProvider'; import InitialURLContextProvider from './components/InitialURLContextProvider'; -import KeyboardProvider from './components/KeyboardProvider'; import {LocaleContextProvider} from './components/LocaleContextProvider'; import OnyxProvider from './components/OnyxProvider'; import PopoverContextProvider from './components/PopoverProvider'; diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 207f3eed6f8c..592db299e3e5 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -491,7 +491,6 @@ function AttachmentModal({ <> { diff --git a/src/components/CategorySelector/CategorySelectorModal.tsx b/src/components/CategorySelector/CategorySelectorModal.tsx index 1362de468014..f12a52dbb314 100644 --- a/src/components/CategorySelector/CategorySelectorModal.tsx +++ b/src/components/CategorySelector/CategorySelectorModal.tsx @@ -42,7 +42,7 @@ function CategorySelectorModal({policyID, isVisible, currentCategory, onCategory diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx index fd2013c6bde7..637a514b53f3 100755 --- a/src/components/ConfirmModal.tsx +++ b/src/components/ConfirmModal.tsx @@ -143,7 +143,6 @@ function ConfirmModal({ return ( {children}; + return {children}; } FixedFooter.displayName = 'FixedFooter'; diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index d26276d0418b..64bb2173f5b0 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -6,10 +6,9 @@ import {Keyboard} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormElement from '@components/FormElement'; -import SafeAreaConsumer from '@components/SafeAreaConsumer'; -import type {SafeAreaChildrenProps} from '@components/SafeAreaConsumer/types'; import ScrollView from '@components/ScrollView'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import type {OnyxFormKey} from '@src/ONYXKEYS'; @@ -60,6 +59,7 @@ function FormWrapper({ isSubmitDisabled = false, }: FormWrapperProps) { const styles = useThemeStyles(); + const {paddingBottom: safeAreaInsetPaddingBottom} = useStyledSafeAreaInsets(); const formRef = useRef(null); const formContentRef = useRef(null); @@ -99,11 +99,12 @@ function FormWrapper({ }, [errors, formState?.errorFields, inputRefs]); const scrollViewContent = useCallback( - (safeAreaPaddingBottomStyle: SafeAreaChildrenProps['safeAreaPaddingBottomStyle']) => ( + () => ( {children} {isSubmitButtonVisible && ( @@ -128,7 +129,8 @@ function FormWrapper({ [ formID, style, - styles.pb5, + safeAreaInsetPaddingBottom, + styles.pb5.paddingBottom, styles.mh0, styles.mt5, styles.flex1, @@ -153,33 +155,27 @@ function FormWrapper({ ); if (!shouldUseScrollView) { - return scrollViewContent({}); + return scrollViewContent(); } - return ( - - {({safeAreaPaddingBottomStyle}) => - scrollContextEnabled ? ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) : ( - - {scrollViewContent(safeAreaPaddingBottomStyle)} - - ) - } - + return scrollContextEnabled ? ( + + {scrollViewContent()} + + ) : ( + + {scrollViewContent()} + ); } diff --git a/src/components/FormAlertWithSubmitButton.tsx b/src/components/FormAlertWithSubmitButton.tsx index 205bea93f84a..fabb5e54cb60 100644 --- a/src/components/FormAlertWithSubmitButton.tsx +++ b/src/components/FormAlertWithSubmitButton.tsx @@ -2,7 +2,6 @@ import type {Ref} from 'react'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle'; import useThemeStyles from '@hooks/useThemeStyles'; import getPlatform from '@libs/getPlatform'; import CONST from '@src/CONST'; @@ -87,7 +86,6 @@ function FormAlertWithSubmitButton({ }: FormAlertWithSubmitButtonProps) { const styles = useThemeStyles(); const style = [!footerContent ? {} : styles.mb3, buttonStyles]; - const safePaddingBottomStyle = useSafePaddingBottomStyle(); // Disable pressOnEnter for Android Native to avoid issues with the Samsung keyboard, // where pressing Enter saves the form instead of adding a new line in multiline input. @@ -97,7 +95,7 @@ function FormAlertWithSubmitButton({ return ( & { /** State that determines whether to display the modal or not */ isVisible: boolean; - /** Callback method fired when the user requests to submit the modal content. */ - onSubmit?: () => void; - /** Callback method fired when the modal is hidden */ onModalHide?: () => void; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 62c1ed22b42c..b577f2be5337 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -1,4 +1,3 @@ -import type {RouteProp} from '@react-navigation/native'; import {useRoute} from '@react-navigation/native'; import lodashSortBy from 'lodash/sortBy'; import truncate from 'lodash/truncate'; @@ -29,6 +28,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -71,7 +71,7 @@ function MoneyRequestPreviewContent({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); - const route = useRoute>(); + const route = useRoute>(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || '-1'}`); diff --git a/src/components/SafeAreaConsumer/index.tsx b/src/components/SafeAreaConsumer/index.tsx index 88770e1f0639..416021242225 100644 --- a/src/components/SafeAreaConsumer/index.tsx +++ b/src/components/SafeAreaConsumer/index.tsx @@ -7,6 +7,7 @@ import type SafeAreaConsumerProps from './types'; /** * This component is a light wrapper around the SafeAreaInsetsContext.Consumer. There are several places where we * may need not just the insets, but the computed styles so we save a few lines of code with this. + * Note: if you're working within a please use `useStyledSafeAreaInsets` instead. */ function SafeAreaConsumer({children}: SafeAreaConsumerProps) { const StyleUtils = useStyleUtils(); diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index c74ccf0470d0..e264b7fd9a55 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -1,5 +1,4 @@ import {UNSTABLE_usePreventRemove, useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; -import type {StackNavigationProp} from '@react-navigation/stack'; import type {ForwardedRef, ReactNode} from 'react'; import React, {createContext, forwardRef, useEffect, useMemo, useRef, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; @@ -11,10 +10,12 @@ import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useKeyboardState from '@hooks/useKeyboardState'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useTackInputFocus from '@hooks/useTackInputFocus'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; +import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; @@ -25,7 +26,6 @@ import HeaderGap from './HeaderGap'; import ImportedStateIndicator from './ImportedStateIndicator'; import KeyboardAvoidingView from './KeyboardAvoidingView'; import OfflineIndicator from './OfflineIndicator'; -import SafeAreaConsumer from './SafeAreaConsumer'; import withNavigationFallback from './withNavigationFallback'; type ScreenWrapperChildrenProps = { @@ -96,7 +96,7 @@ type ScreenWrapperProps = { * * This is required because transitionEnd event doesn't trigger in the testing environment. */ - navigation?: StackNavigationProp | StackNavigationProp; + navigation?: PlatformStackNavigationProp | PlatformStackNavigationProp; /** Whether to show offline indicator on wide screens */ shouldShowOfflineIndicatorInWideScreen?: boolean; @@ -105,7 +105,11 @@ type ScreenWrapperProps = { focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings']; }; -type ScreenWrapperStatusContextType = {didScreenTransitionEnd: boolean}; +type ScreenWrapperStatusContextType = { + didScreenTransitionEnd: boolean; + isSafeAreaTopPaddingApplied: boolean; + isSafeAreaBottomPaddingApplied: boolean; +}; const ScreenWrapperStatusContext = createContext(undefined); @@ -141,7 +145,7 @@ function ScreenWrapper( * so in other places where ScreenWrapper is used, we need to * fallback to useNavigation. */ - const navigationFallback = useNavigation>(); + const navigationFallback = useNavigation>(); const navigation = navigationProp ?? navigationFallback; const isFocused = useIsFocused(); const {windowHeight} = useWindowDimensions(shouldUseCachedViewportHeight); @@ -233,96 +237,86 @@ function ScreenWrapper( // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, []); + const {insets, paddingTop, paddingBottom, safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets(); + const paddingStyle: StyleProp = {}; + + const isSafeAreaTopPaddingApplied = includePaddingTop; + if (includePaddingTop) { + paddingStyle.paddingTop = paddingTop; + } + + // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked. + const isSafeAreaBottomPaddingApplied = includeSafeAreaPaddingBottom || (isOffline && shouldShowOfflineIndicator); + if (isSafeAreaBottomPaddingApplied) { + paddingStyle.paddingBottom = paddingBottom; + } + const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit()); - const contextValue = useMemo(() => ({didScreenTransitionEnd}), [didScreenTransitionEnd]); + const contextValue = useMemo( + () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied}), + [didScreenTransitionEnd, isSafeAreaBottomPaddingApplied, isSafeAreaTopPaddingApplied], + ); return ( - - {({ - insets = { - top: 0, - bottom: 0, - left: 0, - right: 0, - }, - paddingTop, - paddingBottom, - safeAreaPaddingBottomStyle, - }) => { - const paddingStyle: StyleProp = {}; - - if (includePaddingTop) { - paddingStyle.paddingTop = paddingTop; - } - - // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked. - if (includeSafeAreaPaddingBottom || (isOffline && shouldShowOfflineIndicator)) { - paddingStyle.paddingBottom = paddingBottom; - } - - return ( - - + + + + - - - - - {isDevelopment && } - - { - // 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 && ( - <> - - {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} - - - )} - {!shouldUseNarrowLayout && shouldShowOfflineIndicatorInWideScreen && ( - <> - - {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} - - - )} - - - - - - - ); - }} - + + {isDevelopment && } + + { + // 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 && ( + <> + + {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} + + + )} + {!shouldUseNarrowLayout && shouldShowOfflineIndicatorInWideScreen && ( + <> + + {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} + + + )} + + + + + + ); } diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx index d7815d7a65a0..78d8c5ed61fb 100644 --- a/src/components/ScrollOffsetContextProvider.tsx +++ b/src/components/ScrollOffsetContextProvider.tsx @@ -1,7 +1,8 @@ -import type {ParamListBase, RouteProp} from '@react-navigation/native'; +import type {ParamListBase} from '@react-navigation/native'; import React, {createContext, useCallback, useEffect, useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import usePrevious from '@hooks/usePrevious'; +import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {NavigationPartialRoute, State} from '@libs/Navigation/types'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -10,10 +11,10 @@ import type {PriorityMode} from '@src/types/onyx'; type ScrollOffsetContextValue = { /** Save scroll offset of flashlist on given screen */ - saveScrollOffset: (route: RouteProp, scrollOffset: number) => void; + saveScrollOffset: (route: PlatformStackRouteProp, scrollOffset: number) => void; /** Get scroll offset value for given screen */ - getScrollOffset: (route: RouteProp) => number | undefined; + getScrollOffset: (route: PlatformStackRouteProp) => number | undefined; /** Clean scroll offsets of screen that aren't anymore in the state */ cleanStaleScrollOffsets: (state: State) => void; @@ -38,7 +39,7 @@ const defaultValue: ScrollOffsetContextValue = { const ScrollOffsetContext = createContext(defaultValue); /** This function is prepared to work with HOME screens. May need modification if we want to handle other types of screens. */ -function getKey(route: RouteProp | NavigationPartialRoute): string { +function getKey(route: PlatformStackRouteProp | NavigationPartialRoute): string { if (route.params && 'policyID' in route.params && typeof route.params.policyID === 'string') { return `${route.name}-${route.params.policyID}`; } diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index f4feaef632b7..a78845f126d2 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -17,6 +17,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as SearchActions from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates} from '@libs/PolicyUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow'; import variables from '@styles/variables'; @@ -130,7 +131,28 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { setIsOfflineModalVisible(true); return; } + + const activeRoute = Navigation.getActiveRoute(); const transactionIDList = selectedReports.length ? undefined : Object.keys(selectedTransactions); + const items = selectedReports.length ? selectedReports : Object.values(selectedTransactions); + + for (const item of items) { + const policyID = item.policyID; + const lastPolicyPaymentMethod = policyID ? lastPaymentMethods?.[policyID] : null; + + if (!lastPolicyPaymentMethod) { + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: item.reportID, backTo: activeRoute})); + return; + } + + const hasVBBA = PolicyUtils.hasVBBA(policyID); + + if (lastPolicyPaymentMethod !== CONST.IOU.PAYMENT_TYPE.ELSEWHERE && !hasVBBA) { + Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: item.reportID, backTo: activeRoute})); + return; + } + } + const paymentData = ( selectedReports.length ? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentType: lastPaymentMethods[report.policyID]})) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ea712ddaafd6..b2288bac5672 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -1,5 +1,4 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; -import type {StackNavigationProp} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; @@ -21,6 +20,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import memoize from '@libs/memoize'; import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; +import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import * as SearchUIUtils from '@libs/SearchUIUtils'; @@ -108,7 +108,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact action: item.action, reportID: item.reportID, policyID: item.policyID, - amount: item.modifiedAmount ?? item.amount, + amount: Math.abs(item.modifiedAmount || item.amount), }, }; } @@ -120,7 +120,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for enabling the selection mode on small screens only // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth, isLargeScreenWidth} = useResponsiveLayout(); - const navigation = useNavigation>(); + const navigation = useNavigation>(); const isFocused = useIsFocused(); const [lastNonEmptySearchResults, setLastNonEmptySearchResults] = useState(undefined); const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions, setShouldShowStatusBarLoading, lastSearchType, setLastSearchType} = diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 58e1bc0e32fe..87d2b2c8af29 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -10,7 +10,6 @@ import Checkbox from '@components/Checkbox'; import FixedFooter from '@components/FixedFooter'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; import {PressableWithFeedback} from '@components/Pressable'; -import SafeAreaConsumer from '@components/SafeAreaConsumer'; import SectionList from '@components/SectionList'; import ShowMoreButton from '@components/ShowMoreButton'; import Text from '@components/Text'; @@ -22,6 +21,7 @@ import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useSingleExecution from '@hooks/useSingleExecution'; +import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; import getSectionsWithIndexOffset from '@libs/getSectionsWithIndexOffset'; import Log from '@libs/Log'; @@ -635,7 +635,7 @@ function BaseSelectionList( if (shouldDelayFocus) { focusTimeoutRef.current = setTimeout(focusTextInput, CONST.ANIMATED_TRANSITION); } else { - focusTextInput(); + requestAnimationFrame(focusTextInput); } } @@ -768,91 +768,89 @@ function BaseSelectionList( }, ); + const {safeAreaPaddingBottomStyle} = useStyledSafeAreaInsets(); + + // TODO: test _every_ component that uses SelectionList return ( - - {({safeAreaPaddingBottomStyle}) => ( - - {shouldShowTextInput && !shouldShowTextInputAfterHeader && renderInput()} - {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} - {/* This is misleading because we might be in the process of loading fresh options from the server. */} - {(!isLoadingNewOptions || headerMessage !== translate('common.noResultsFound') || (flattenedSections.allOptions.length === 0 && !showLoadingPlaceholder)) && - !!headerMessage && ( - - {headerMessage} - - )} - {!!headerContent && headerContent} - {flattenedSections.allOptions.length === 0 && (showLoadingPlaceholder || shouldShowListEmptyContent) ? ( - renderListEmptyContent() - ) : ( - <> - {!listHeaderContent && header()} - ( - <> - {renderSectionHeader(arg)} - {listHeaderContent && header()} - - )} - renderItem={renderItem} - getItemLayout={getItemLayout} - onScroll={onScroll} - onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item, index) => item.keyForList ?? `${index}`} - extraData={focusedIndex} - // the only valid values on the new arch are "white", "black", and "default", other values will cause a crash - indicatorStyle="white" - keyboardShouldPersistTaps="always" - showsVerticalScrollIndicator={showScrollIndicator} - initialNumToRender={12} - maxToRenderPerBatch={maxToRenderPerBatch} - windowSize={windowSize} - updateCellsBatchingPeriod={updateCellsBatchingPeriod} - viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}} - testID="selection-list" - onLayout={onSectionListLayout} - style={[(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0, sectionListStyle]} - ListHeaderComponent={ - shouldShowTextInput && shouldShowTextInputAfterHeader ? ( - <> - {listHeaderContent} - {renderInput()} - - ) : ( - listHeaderContent - ) - } - ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance} - onEndReached={onEndReached} - onEndReachedThreshold={onEndReachedThreshold} - scrollEventThrottle={scrollEventThrottle} - contentContainerStyle={contentContainerStyle} - CellRendererComponent={shouldPreventActiveCellVirtualization ? FocusAwareCellRendererComponent : undefined} - /> - {children} - - )} - {showConfirmButton && ( - -