import { findFocusedRoute , getActionFromState as getActionFromStateDefault , getPathFromState as getPathFromStateDefault , getStateFromPath as getStateFromPathDefault , NavigationContainerRef , NavigationState , ParamListBase , } from '@react-navigation/core' ; import { nanoid } from 'nanoid/non-secure' ; import * as React from 'react' ; import ServerContext from './ServerContext' ; import type { LinkingOptions } from './types' ; type ResultState = ReturnType < typeof getStateFromPathDefault > ; type HistoryRecord = { id : string ; state : NavigationState ; path : string ; } ; const createMemoryHistory = ( ) => { let index = 0 ; let items = [ ] ; const pending = [ ] ; const interrupt = ( ) => { pending . forEach ( ( it ) => { const cb = it . cb ; it . cb = ( ) => cb ( true ) ; } ) ; } ; const history = { get index ( ) : number { const id = window . history . state ?. id ; if ( id ) { const index = items . findIndex ( ( item ) => item . id === id ) ; return index > - 1 ? index : 0 ; } return 0 ; } , get ( index ) { return items [ index ] ; } , backIndex ( { path } ) { for ( let i = index - 1 ; i >= 0 ; i -- ) { const item = items [ i ] ; if ( item . path === path ) { return i ; } } return - 1 ; } , push ( { path , state } ) { interrupt ( ) ; const id = nanoid ( ) ; items = items . slice ( 0 , index + 1 ) ; items . push ( { path , state , id } ) ; index = items . length - 1 ; window . history . pushState ( { id } , '' , path ) ; } , replace ( { path , state } ) { interrupt ( ) ; const id = window . history . state ?. id ?? nanoid ( ) ; if ( ! items . length || items . findIndex ( ( item ) => item . id === id ) < 0 ) { items = [ { path , state , id } ] ; } else { items [ index ] = { path , state , id } ; } window . history . replaceState ( { id } , '' , path ) ; } , go ( n ) { interrupt ( ) ; if ( n > 0 ) { n = Math . min ( n , items . length - 1 ) ; } else if ( n < 0 ) { n = index + n < 0 ? - index : n ; } if ( n === 0 ) { return ; } index += n ; return new Promise < void > ( ( resolve , reject ) => { const done = ( interrupted ? ) => { clearTimeout ( timer ) ; if ( interrupted ) { reject ( new Error ( 'History was changed during navigation.' ) ) ; return ; } const { title } = window . document ; window . document . title = '' ; window . document . title = title ; resolve ( ) ; } ; pending . push ( { ref : done , cb : done } ) ; const timer = setTimeout ( ( ) => { const index = pending . findIndex ( ( it ) => it . ref === done ) ; if ( index > - 1 ) { pending [ index ] . cb ( ) ; pending . splice ( index , 1 ) ; } } , 100 ) ; const onPopState = ( ) => { const id = window . history . state ?. id ; const currentIndex = items . findIndex ( ( item ) => item . id === id ) ; index = Math . max ( currentIndex , 0 ) ; const last = pending . pop ( ) ; window . removeEventListener ( 'popstate' , onPopState ) ; last ?. cb ( ) ; } ; window . addEventListener ( 'popstate' , onPopState ) ; window . history . go ( n ) ; } ) ; } , listen ( listener ) { const onPopState = ( ) => { if ( pending . length ) { return ; } listener ( ) ; } ; window . addEventListener ( 'popstate' , onPopState ) ; return ( ) => window . removeEventListener ( 'popstate' , onPopState ) ; } , } ; return history ; } ; /**
 * Find the matching navigation state that changed between 2 navigation states
 * e.g.: a -> b -> c -> d and a -> b -> c -> e -> f, if history in b changed, b is the matching state
 */ const findMatchingState = < T extends NavigationState > ( a , b ) : [ T | undefined , T | undefined ] => { if ( a === undefined || b === undefined || a . key !== b . key ) { return [ undefined , undefined ] ; } const aHistoryLength = a . history ? a . history . length : a . routes . length ; const bHistoryLength = b . history ? b . history . length : b . routes . length ; const aRoute = a . routes [ a . index ] ; const bRoute = b . routes [ b . index ] ; const aChildState = aRoute . state as T | undefined ; const bChildState = bRoute . state as T | undefined ; if ( aHistoryLength !== bHistoryLength || aRoute . key !== bRoute . key || aChildState === undefined || bChildState === undefined || aChildState . key !== bChildState . key ) { return [ a , b ] ; } return findMatchingState ( aChildState , bChildState ) ; } ; /**
 * Run async function in series as it's called.
 */ const series = ( cb ) => { let handling = false ; let queue = [ ] ; const callback = async ( ) => { try { if ( handling ) { queue . unshift ( callback ) ; return ; } handling = true ; await cb ( ) ; } finally { handling = false ; if ( queue . length ) { const last = queue . pop ( ) ; last ?. ( ) ; } } } ; return callback ; } ; let linkingHandlers = [ ] ; type Options = LinkingOptions < ParamListBase > & { independent ? : boolean ; } ; export default function useLinking ( ref , { independent , enabled = true , config , getStateFromPath = getStateFromPathDefault , getPathFromState = getPathFromStateDefault , getActionFromState = getActionFromStateDefault , } ) { React . useEffect ( ( ) => { if ( process . env . NODE_ENV === 'production' ) { return undefined ; } if ( independent ) { return undefined ; } if ( enabled !== false && linkingHandlers . length ) { console . error ( [ 'Looks like you have configured linking in multiple places. This is likely an error since deep links should only be handled in one place to avoid conflicts. Make sure that:' , "- You don't have multiple NavigationContainers in the app each with 'linking' enabled" , '- Only a single instance of the root component is rendered' , ] . join ( '\n' ) . trim ( ) ) ; } const handler = Symbol ( ) ; if ( enabled !== false ) { linkingHandlers . push ( handler ) ; } return ( ) => { const index = linkingHandlers . indexOf ( handler ) ; if ( index > - 1 ) { linkingHandlers . splice ( index , 1 ) ; } } ; } , [ enabled , independent ] ) ; const [ history ] = React . useState ( createMemoryHistory ) ; const enabledRef = React . useRef ( enabled ) ; const configRef = React . useRef ( config ) ; const getStateFromPathRef = React . useRef ( getStateFromPath ) ; const getPathFromStateRef = React . useRef ( getPathFromState ) ; const getActionFromStateRef = React . useRef ( getActionFromState ) ; React . useEffect ( ( ) => { enabledRef . current = enabled ; configRef . current = config ; getStateFromPathRef . current = getStateFromPath ; getPathFromStateRef . current = getPathFromState ; getActionFromStateRef . current = getActionFromState ; } ) ; const server = React . useContext ( ServerContext ) ; const getInitialState = React . useCallback ( ( ) => { let value ; if ( enabledRef . current ) { const location = server ?. location ?? ( typeof window !== 'undefined' ? window . location : undefined ) ; const path = location ? location . pathname + location . search : undefined ; if ( path ) { value = getStateFromPathRef . current ( path , configRef . current ) ; } } const thenable = { then ( onfulfilled ? ) { return Promise . resolve ( onfulfilled ? onfulfilled ( value ) : value ) ; } , catch ( ) { return thenable ; } , } ; return thenable as PromiseLike < ResultState | undefined > ; } , [ ] ) ; const previousIndexRef = React . useRef < number | undefined > ( undefined ) ; const previousStateRef = React . useRef < NavigationState | undefined > ( undefined ) ; const pendingPopStatePathRef = React . useRef < string | undefined > ( undefined ) ; React . useEffect ( ( ) => { previousIndexRef . current = history . index ; return history . listen ( ( ) => { const navigation = ref . current ; if ( ! navigation || ! enabled ) { return ; } const path = location . pathname + location . search ; const index = history . index ; const previousIndex = previousIndexRef . current ?? 0 ; previousIndexRef . current = index ; pendingPopStatePathRef . current = path ; const record = history . get ( index ) ; if ( record ?. path === path && record ?. state ) { navigation . resetRoot ( record . state ) ; return ; } const state = getStateFromPathRef . current ( path , configRef . current ) ; if ( state ) { const rootState = navigation . getRootState ( ) ; if ( state . routes . some ( ( r ) => ! rootState ?. routeNames . includes ( r . name ) ) ) { console . warn ( "The navigation state parsed from the URL contains routes not present in the root navigator. This usually means that the linking configuration doesn't match the navigation structure. See https://reactnavigation.org/docs/configuring-links for more details on how to specify a linking configuration." ) ; return ; } if ( index > previousIndex ) { const action = getActionFromStateRef . current ( state , configRef . current ) ; if ( action !== undefined ) { try { navigation . dispatch ( action ) ; } catch ( e ) { console . warn ( `An error occurred when trying to handle the link '${ path }': ${ e . message }` ) ; } } else { navigation . resetRoot ( state ) ; } } else { navigation . resetRoot ( state ) ; } } else { navigation . resetRoot ( state ) ; } } ) ; } , [ enabled , history , ref ] ) ; React . useEffect ( ( ) => { if ( ! enabled ) { return ; } if ( ref . current ) { const state = ref . current . getRootState ( ) ; if ( state ) { const route = findFocusedRoute ( state ) ; const path = route ?. path ?? getPathFromStateRef . current ( state , configRef . current ) ; if ( previousStateRef . current === undefined ) { previousStateRef . current = state ; } history . replace ( { path , state } ) ; } } const onStateChange = async ( ) => { const navigation = ref . current ; if ( ! navigation || ! enabled ) { return ; } const previousState = previousStateRef . current ; const state = navigation . getRootState ( ) ; const pendingPath = pendingPopStatePathRef . current ; const route = findFocusedRoute ( state ) ; const path = route ?. path ?? getPathFromStateRef . current ( state , configRef . current ) ; previousStateRef . current = state ; pendingPopStatePathRef . current = undefined ; const [ previousFocusedState , focusedState ] = findMatchingState ( previousState , state ) ; if ( previousFocusedState && focusedState && path !== pendingPath ) { const historyDelta = ( focusedState . history ? focusedState . history . length : focusedState . routes . length ) - ( previousFocusedState . history ? previousFocusedState . history . length : previousFocusedState . routes . length ) ; if ( historyDelta > 0 ) { history . push ( { path , state } ) ; } else if ( historyDelta < 0 ) { const nextIndex = history . backIndex ( { path } ) ; const currentIndex = history . index ; try { if ( nextIndex !== - 1 && nextIndex < currentIndex ) { await history . go ( nextIndex - currentIndex ) ; } else { await history . go ( historyDelta ) ; } history . replace ( { path , state } ) ; } catch ( e ) { } } else { history . replace ( { path , state } ) ; } } else { history . replace ( { path , state } ) ; } } ; return ref . current ?. addListener ( 'state' , series ( onStateChange ) ) ; } ) ; return { getInitialState , } ; }