import React, { PropsWithChildren, ReactNode } from 'react'; import { Animated, Image, ImageProps, Platform, requireNativeComponent, StyleProp, StyleSheet, UIManager, View, ViewProps, ViewStyle, } from 'react-native'; import { Freeze } from 'react-freeze'; import { version } from 'react-native/package.json'; import TransitionProgressContext from './TransitionProgressContext'; import useTransitionProgress from './useTransitionProgress'; import { StackPresentationTypes, StackAnimationTypes, BlurEffectTypes, ScreenReplaceTypes, ScreenOrientationTypes, HeaderSubviewTypes, ScreenProps, ScreenContainerProps, ScreenStackProps, ScreenStackHeaderConfigProps, SearchBarProps, } from './types'; import { isSearchBarAvailableForCurrentPlatform, executeNativeBackPress, } from './utils'; // web implementation is taken from `index.tsx` const isPlatformSupported = Platform.OS === 'ios' || Platform.OS === 'android' || Platform.OS === 'windows'; let ENABLE_SCREENS = isPlatformSupported; function enableScreens(shouldEnableScreens = true): void { ENABLE_SCREENS = isPlatformSupported && shouldEnableScreens; if (ENABLE_SCREENS && !UIManager.getViewManagerConfig('RNSScreen')) { console.error( `Screen native module hasn't been linked. Please check the react-native-screens README for more details` ); } } let ENABLE_FREEZE = false; function enableFreeze(shouldEnableReactFreeze = true): void { const minor = parseInt(version.split('.')[1]); // eg. takes 66 from '0.66.0' // react-freeze requires react-native >=0.64, react-native from main is 0.0.0 if (!(minor === 0 || minor >= 64) && shouldEnableReactFreeze) { console.warn( 'react-freeze library requires at least react-native 0.64. Please upgrade your react-native version in order to use this feature.' ); } ENABLE_FREEZE = shouldEnableReactFreeze; } // const that tells if the library should use new implementation, will be undefined for older versions const shouldUseActivityState = true; function screensEnabled(): boolean { return ENABLE_SCREENS; } // We initialize these lazily so that importing the module doesn't throw error when not linked // This is necessary coz libraries such as React Navigation import the library where it may not be enabled let NativeScreenValue: React.ComponentType; let NativeScreenContainerValue: React.ComponentType; let NativeScreenNavigationContainerValue: React.ComponentType; let NativeScreenStack: React.ComponentType; let NativeScreenStackHeaderConfig: React.ComponentType; let NativeScreenStackHeaderSubview: React.ComponentType< React.PropsWithChildren >; let AnimatedNativeScreen: React.ComponentType; let NativeSearchBar: React.ComponentType; let NativeFullWindowOverlay: React.ComponentType< PropsWithChildren<{ style: StyleProp; }> >; const ScreensNativeModules = { get NativeScreen() { NativeScreenValue = NativeScreenValue || requireNativeComponent('RNSScreen'); return NativeScreenValue; }, get NativeScreenContainer() { NativeScreenContainerValue = NativeScreenContainerValue || requireNativeComponent('RNSScreenContainer'); return NativeScreenContainerValue; }, get NativeScreenNavigationContainer() { NativeScreenNavigationContainerValue = NativeScreenNavigationContainerValue || (Platform.OS === 'ios' ? requireNativeComponent('RNSScreenNavigationContainer') : this.NativeScreenContainer); return NativeScreenNavigationContainerValue; }, get NativeScreenStack() { NativeScreenStack = NativeScreenStack || requireNativeComponent('RNSScreenStack'); return NativeScreenStack; }, get NativeScreenStackHeaderConfig() { NativeScreenStackHeaderConfig = NativeScreenStackHeaderConfig || requireNativeComponent('RNSScreenStackHeaderConfig'); return NativeScreenStackHeaderConfig; }, get NativeScreenStackHeaderSubview() { NativeScreenStackHeaderSubview = NativeScreenStackHeaderSubview || requireNativeComponent('RNSScreenStackHeaderSubview'); return NativeScreenStackHeaderSubview; }, get NativeSearchBar() { NativeSearchBar = NativeSearchBar || requireNativeComponent('RNSSearchBar'); return NativeSearchBar; }, get NativeFullWindowOverlay() { NativeFullWindowOverlay = NativeFullWindowOverlay || requireNativeComponent('RNSFullWindowOverlay'); return NativeFullWindowOverlay; }, }; interface FreezeWrapperProps { freeze: boolean; children: React.ReactNode; } // This component allows one more render before freezing the screen. // Allows activityState to reach the native side and useIsFocused to work correctly. function DelayedFreeze({ freeze, children }: FreezeWrapperProps) { // flag used for determining whether freeze should be enabled const [freezeState, setFreezeState] = React.useState(false); if (freeze !== freezeState) { // setImmediate is executed at the end of the JS execution block. // Used here for changing the state right after the render. setImmediate(() => { setFreezeState(freeze); }); } return {children}; } function ScreenStack(props: ScreenStackProps) { const { children, ...rest } = props; const size = React.Children.count(children); // freezes all screens except the top one const childrenWithFreeze = React.Children.map(children, (child, index) => { // @ts-expect-error it's either SceneView in v6 or RouteView in v5 const { props, key } = child; const descriptor = props?.descriptor ?? props?.descriptors?.[key]; const freezeEnabled = descriptor?.options?.freezeOnBlur ?? ENABLE_FREEZE; return ( 1}> {child} ); }); return ( {childrenWithFreeze} ); } // Incomplete type, all accessible properties available at: // react-native/Libraries/Components/View/ReactNativeViewViewConfig.js interface ViewConfig extends View { viewConfig: { validAttributes: { style: { display: boolean; }; }; }; } class InnerScreen extends React.Component { private ref: React.ElementRef | null = null; private closing = new Animated.Value(0); private progress = new Animated.Value(0); private goingForward = new Animated.Value(0); setNativeProps(props: ScreenProps): void { this.ref?.setNativeProps(props); } setRef = (ref: React.ElementRef | null): void => { this.ref = ref; this.props.onComponentRef?.(ref); }; render() { const { enabled = ENABLE_SCREENS, freezeOnBlur = ENABLE_FREEZE, ...rest } = this.props; if (enabled && isPlatformSupported) { AnimatedNativeScreen = AnimatedNativeScreen || Animated.createAnimatedComponent(ScreensNativeModules.NativeScreen); let { // Filter out active prop in this case because it is unused and // can cause problems depending on react-native version: // https://github.com/react-navigation/react-navigation/issues/4886 active, activityState, children, isNativeStack, gestureResponseDistance, ...props } = rest; if (active !== undefined && activityState === undefined) { console.warn( 'It appears that you are using old version of react-navigation library. Please update @react-navigation/bottom-tabs, @react-navigation/stack and @react-navigation/drawer to version 5.10.0 or above to take full advantage of new functionality added to react-native-screens' ); activityState = active !== 0 ? 2 : 0; // in the new version, we need one of the screens to have value of 2 after the transition } const handleRef = (ref: ViewConfig) => { if (ref?.viewConfig?.validAttributes?.style) { ref.viewConfig.validAttributes.style = { ...ref.viewConfig.validAttributes.style, display: false, }; this.setRef(ref); } }; return ( {!isNativeStack ? ( // see comment of this prop in types.tsx for information why it is needed children ) : ( {children} )} ); } else { // same reason as above let { active, activityState, style, // eslint-disable-next-line @typescript-eslint/no-unused-vars onComponentRef, ...props } = rest; if (active !== undefined && activityState === undefined) { activityState = active !== 0 ? 2 : 0; } return ( ); } } } function ScreenContainer(props: ScreenContainerProps) { const { enabled = ENABLE_SCREENS, hasTwoStates, ...rest } = props; if (enabled && isPlatformSupported) { if (hasTwoStates) { return ; } return ; } return ; } function FullWindowOverlay(props: { children: ReactNode }) { if (Platform.OS !== 'ios') { console.warn('Importing FullWindowOverlay is only valid on iOS devices.'); return ; } return ( {props.children} ); } const styles = StyleSheet.create({ headerSubview: { position: 'absolute', top: 0, right: 0, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', }, }); const ScreenStackHeaderBackButtonImage = (props: ImageProps): JSX.Element => ( ); const ScreenStackHeaderRightView = ( props: React.PropsWithChildren ): JSX.Element => ( ); const ScreenStackHeaderLeftView = ( props: React.PropsWithChildren ): JSX.Element => ( ); const ScreenStackHeaderCenterView = ( props: React.PropsWithChildren ): JSX.Element => ( ); const ScreenStackHeaderSearchBarView = ( props: React.PropsWithChildren ): JSX.Element => ( ); export type { StackPresentationTypes, StackAnimationTypes, BlurEffectTypes, ScreenReplaceTypes, ScreenOrientationTypes, HeaderSubviewTypes, ScreenProps, ScreenContainerProps, ScreenStackProps, ScreenStackHeaderConfigProps, SearchBarProps, }; // context to be used when the user wants to use enhanced implementation // e.g. to use `useReanimatedTransitionProgress` (see `reanimated` folder in repo) const ScreenContext = React.createContext(InnerScreen); class Screen extends React.Component { static contextType = ScreenContext; render() { const ScreenWrapper = this.context || InnerScreen; return ; } } module.exports = { // these are classes so they are not evaluated until used // so no need to use getters for them Screen, ScreenContainer, ScreenContext, ScreenStack, InnerScreen, FullWindowOverlay, get NativeScreen() { return ScreensNativeModules.NativeScreen; }, get NativeScreenContainer() { return ScreensNativeModules.NativeScreenContainer; }, get NativeScreenNavigationContainer() { return ScreensNativeModules.NativeScreenNavigationContainer; }, get ScreenStackHeaderConfig() { return ScreensNativeModules.NativeScreenStackHeaderConfig; }, get ScreenStackHeaderSubview() { return ScreensNativeModules.NativeScreenStackHeaderSubview; }, get SearchBar() { if (!isSearchBarAvailableForCurrentPlatform) { console.warn( 'Importing SearchBar is only valid on iOS and Android devices.' ); return View; } return ScreensNativeModules.NativeSearchBar; }, // these are functions and will not be evaluated until used // so no need to use getters for them ScreenStackHeaderBackButtonImage, ScreenStackHeaderRightView, ScreenStackHeaderLeftView, ScreenStackHeaderCenterView, ScreenStackHeaderSearchBarView, enableScreens, enableFreeze, screensEnabled, shouldUseActivityState, useTransitionProgress, isSearchBarAvailableForCurrentPlatform, executeNativeBackPress, };