<|start_filename|>src/screen.js<|end_filename|>
import uuid from 'uuid/v4'

const defaultTransitionProps = {
    animation: 'right',
    duration: 250,
    easing: 'ease-in-out'
}

const isDefinedByString = screen => typeof screen === 'string'

export const createScreen = screen => {
    const normilized = isDefinedByString(screen)
        ? { props: {}, screen, transitionProps: defaultTransitionProps }
        : {
              props: screen.props || {},
              screen: screen.screen,
              transitionProps: !!screen.transitionProps
                  ? { ...defaultTransitionProps, ...screen.transitionProps }
                  : defaultTransitionProps
          }
    return { id: uuid(), ...normilized }
}

export const createInitialStack = (initialStack, screens) => {
    return isDefinedByString(initialStack)
        ? [createScreen(initialStack)]
        : initialStack.map(createScreen)
}

export const screenStyle = {
    backgroundColor: 'white',
    height: '100%',
    position: 'absolute',
    width: '100%'
}


<|start_filename|>src/index.js<|end_filename|>
import { BackHandler } from 'react-native'
import { createInitialStack, createScreen, screenStyle } from './screen'
import createRunTransition from './transition'
import React from 'react'
import SwipeRecognizer from './swipe'
import { View } from 'react-native-animatable'

class NavigatorScreen extends React.Component {
    shouldComponentUpdate = () => {
        return false
    }

    render = () => {
        const { navigator, passedProps, screen: Screen } = this.props
        return <Screen navigator={navigator} {...passedProps} />
    }
}

class Navigator extends React.Component {
    state = { stack: createInitialStack(this.props.initialStack, this.props.screens) }
    renderedScreens = {}
    runTransition = createRunTransition(this.props.animations)
    inTransition = false
    backHandlers = {}

    updateStack = stack => {
        const { stack: previousStack } = this.state
        return new Promise(resolve =>
            this.setState({ stack }, () => {
                const { onStackUpdate } = this.props
                if (onStackUpdate) onStackUpdate(stack, previousStack)
                resolve()
            })
        )
    }

    navigatorAction = action =>
        new Promise((resolve, reject) => {
            if (this.inTransition) {
                const inTransitionError =
                    "Can't process action when navigator is in transition state"
                return reject(inTransitionError)
            }

            this.inTransition = true
            action(
                () => {
                    this.inTransition = false
                    resolve()
                },
                error => {
                    this.inTransition = false
                    reject(error)
                }
            )
        })
    navigator = {
        pop: transitionProps => {
            if (this.state.stack.length === 1) return

            return this.navigatorAction(async onFinish => {
                const screen = this.state.stack[this.state.stack.length - 1]
                await this.runTransition(
                    this.renderedScreens[screen.id],
                    !!transitionProps
                        ? { ...screen.transitionProps, ...transitionProps }
                        : screen.transitionProps,
                    true
                )
                await this.updateStack(this.state.stack.slice(0, -1))
                onFinish()
            })
        },
        popTo: (screenId, transitionProps) => {
            const screenIndex = this.state.stack.findIndex(({ id }) => id === screenId)
            if (screenIndex < 0) throw new Error(`No screen with id "${screenId}" found`)
            if (screenIndex === this.state.stack.length - 1)
                throw new Error(`Can't pop to current screen`)

            return this.navigatorAction(async (onFinish, onFail) => {
                if (this.state.stack.length === 1)
                    return onFail("Can't pop if there's only one screen in the stack")

                const screen = this.state.stack[this.state.stack.length - 1]
                await this.runTransition(
                    this.renderedScreens[screen.id],
                    !!transitionProps
                        ? { ...screen.transitionProps, ...transitionProps }
                        : screen.transitionProps,
                    true
                )
                await this.updateStack(this.state.stack.slice(0, screenIndex + 1))
                onFinish()
            })
        },
        push: (screenName, props, transitionProps) => {
            if (!this.props.screens.hasOwnProperty(screenName))
                throw new Error(`Screen ${screenName} doesn't exist`)

            return this.navigatorAction(async onFinish => {
                const screen = createScreen({ screen: screenName, props, transitionProps })
                await this.updateStack([...this.state.stack, screen])
                await this.runTransition(
                    this.renderedScreens[screen.id],
                    screen.transitionProps,
                    false
                )
                onFinish()
            })
        },
        reset: (screenName, props, transitionProps) => {
            if (!this.props.screens.hasOwnProperty(screenName))
                throw new Error(`Screen ${screenName} doesn't exist`)

            return this.navigatorAction(async onFinish => {
                const screen = createScreen({ screen: screenName, props, transitionProps })
                await this.updateStack([...this.state.stack, screen])
                await this.runTransition(
                    this.renderedScreens[screen.id],
                    screen.transitionProps,
                    false
                )
                await this.updateStack([screen])
                onFinish()
            })
        },
        resetFrom: (screenId, screenName, props, transitionProps) => {
            if (!this.props.screens.hasOwnProperty(screenName))
                throw new Error(`Screen ${screenName} doesn't exist`)
            const screenIndex = this.state.stack.findIndex(({ id }) => id === screenId)
            if (screenIndex < 0) throw new Error(`No screen with id "${screenId}" found`)

            return this.navigatorAction(async onFinish => {
                const screen = createScreen({ screen: screenName, props, transitionProps })
                await this.updateStack([...this.state.stack, screen])
                await this.runTransition(
                    this.renderedScreens[screen.id],
                    screen.transitionProps,
                    false
                )

                await this.updateStack([...this.state.stack.slice(0, screenIndex + 1), screen])
                onFinish()
            })
        }
    }

    onBackPress = () => {
        if (this.inTransition) return

        const { stack } = this.state
        const lastStackItemId = stack[stack.length - 1].id
        if (this.backHandlers.hasOwnProperty(lastStackItemId)) {
            return this.backHandlers[lastStackItemId](this.navigator) || stack.length > 1
        }

        const { backHandler } = this.props
        if (backHandler) {
            return backHandler(this.navigator) || stack.length > 1
        }

        return stack.length > 1
    }

    UNSAFE_componentWillMount = () => {
        this.androidBackHandler = BackHandler.addEventListener(
            'hardwareBackPress',
            this.onBackPress
        )

        Object.defineProperty(this.navigator, 'stack', { get: () => this.state.stack })

        const { navigatorRef } = this.props
        if (!!navigatorRef) navigatorRef(this.navigator)
    }

    componentWillUnmount = () => {
        this.androidBackHandler.remove()

        const { navigatorRef } = this.props
        if (!!navigatorRef) navigatorRef(undefined)
    }

    renderScreen = (stackItem, index) => {
        const { screens } = this.props
        const { stack } = this.state
        const Screen = screens[stackItem.screen]
        const screenDependentMethods = {
            registerBackHandler: handler => (this.backHandlers[stackItem.id] = handler),
            unregisterBackHandler: () => delete this.backHandlers[stackItem.id]
        }
        const screenNavigator = { ...this.navigator, ...screenDependentMethods }
        Object.defineProperty(screenNavigator, 'stack', { get: () => this.state.stack })

        return (
            <View
                key={stackItem.id}
                pointerEvents={index < stack.length - 1 ? 'none' : undefined}
                style={[screenStyle, { opacity: index < stack.length - 2 ? 0 : 1 }]}
                ref={ref => (this.renderedScreens[stackItem.id] = ref)}
                useNativeDriver={true}>
                <NavigatorScreen
                    navigator={screenNavigator}
                    passedProps={stackItem.props}
                    screen={Screen}
                />
            </View>
        )
    }

    render = () => (
        <SwipeRecognizer onSwipeBack={this.onBackPress}>
            {this.state.stack.map(this.renderScreen)}
        </SwipeRecognizer>
    )
}

Navigator.defaultProps = {
    backHandler: navigator => navigator.pop()
}

export default Navigator


<|start_filename|>src/transition.js<|end_filename|>
import { Dimensions } from 'react-native'

const { width: windowWidth, height: windowHeight } = Dimensions.get('window')
const defaultAnimations = {
    bottom: {
        start: { transform: [{ translateY: windowHeight }] },
        end: { transform: [{ translateY: 0 }] }
    },
    fade: {
        start: { transform: [{ opacity: 0 }] },
        end: { transform: [{ opacity: 1 }] }
    },
    left: {
        start: { transform: [{ translateX: -windowWidth }] },
        end: { transform: [{ translateX: 0 }] }
    },
    right: {
        start: { transform: [{ translateX: windowWidth }] },
        end: { transform: [{ translateX: 0 }] }
    },
    top: {
        start: { transform: [{ translateY: -windowHeight }] },
        end: { transform: [{ translateY: 0 }] }
    }
}

export default customAnimations => {
    const animations = { ...defaultAnimations, ...customAnimations }
    return (screen, transitionProps, reverse) => {
        if (transitionProps.animation === 'none') return Promise.resolve()

        const animation = animations[transitionProps.animation]
        const from = animation[reverse ? 'end' : 'start']
        const to = animation[reverse ? 'start' : 'end']
        const duration = transitionProps.duration
        const easing = transitionProps.easing

        return new Promise(resolve => {
            screen.transition(from, to, duration, easing)
            setTimeout(resolve, duration)
        })
    }
}


<|start_filename|>src/swipe.js<|end_filename|>
import { PanResponder, Platform, View } from 'react-native'
import React from 'react'

const velocityThreshold = 2
const directionalOffsetThreshold = 80

const AndroidPlaceholder = ({ children }) => <View style={{ flex: 1 }}>{children}</View>

class IOSSwipeRecognizer extends React.Component {
    isSwipeBack = gesture =>
        Math.abs(gesture.vx) > velocityThreshold &&
        Math.abs(gesture.dy) < directionalOffsetThreshold &&
        gesture.dx > 0

    check = (event, gesture) => {
        const isSingleTouch = event.nativeEvent.touches.length === 1
        const isClick = Math.abs(gesture.dx) < 5 && Math.abs(gesture.dy) < 5

        return isSingleTouch && !isClick && this.isSwipeBack(gesture)
    }

    finish = (event, gesture) => {
        const { onSwipeBack } = this.props
        if (this.isSwipeBack(gesture)) onSwipeBack()
    }

    panResponser = PanResponder.create({
        onStartShouldSetPanResponder: this.check,
        onMoveShouldSetPanResponder: this.check,
        onPanResponderRelease: this.finish,
        onPanResponderTerminate: this.finish
    })

    render = () => {
        const { children } = this.props
        return (
            <View style={{ flex: 1 }} {...this.panResponser.panHandlers}>
                {children}
            </View>
        )
    }
}

export default Platform.select({
    android: AndroidPlaceholder,
    ios: IOSSwipeRecognizer
})


<|start_filename|>example/index.js<|end_filename|>
import { AppRegistry } from 'react-native'
import { name } from './app.json'
import Navigator from 'react-native-easy-router'
import React from 'react'
import { SafeAreaView, Text, View } from 'react-native'

const ScreenContainer = ({ children, color, title }) => (
    <SafeAreaView style={{ flex: 1 }}>
        <View
            style={{
                alignItems: 'center',
                backgroundColor: 'lightblue',
                flexDirection: 'column',
                justifyContent: 'center',
                height: 50
            }}>
            <Text>{title}</Text>
        </View>
        <View
            style={{
                alignItems: 'center',
                backgroundColor: color,
                flex: 1,
                flexDirection: 'column',
                justifyContent: 'space-around'
            }}>
            {children}
        </View>
    </SafeAreaView>
)

const Screen1 = ({ navigator }) => (
    <ScreenContainer color='lightgreen' title='Screen1'>
        <Text onPress={() => navigator.push('Screen2', { name: 'John' })}>Push Screen2</Text>
    </ScreenContainer>
)

const Screen2 = ({ name, navigator }) => (
    <ScreenContainer color='pink' title='Screen2'>
        <Text>Hello {name}</Text>
        <Text onPress={() => navigator.pop()}>Pop</Text>
    </ScreenContainer>
)

const Application = () => <Navigator initialStack='Screen1' screens={{ Screen1, Screen2 }} />

AppRegistry.registerComponent(name, () => Application)
