Fix deep link URL re-processing on account switch and back gesture reliability
Two fixes for initialUrl/deep link handling:
1. Prevent initial URL from being re-processed on account switch. The
key={currentAccount?.did} in App.native.tsx remounts NavigationContainer,
which re-calls getInitialURL() and navigates to the deep link again.
Fixed by tracking consumption in a module-level flag and providing a
custom getInitialURL that returns null after first call.
2. Fix back swipe gesture not working reliably from deep links. Two changes:
- Changed isStateAtTabRoot to return false when state is undefined
(during init), preventing the Drawer gesture from activating during
the async window before the initial URL resolves.
- Added custom subscribe handler that navigates (pushes) on native
instead of letting React Navigation reset the entire state, ensuring
proper native back gesture setup.
https://claude.ai/code/session_01NCogJsPSFXu8YJG8dWwEbC
This commit is contained in:
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '@react-navigation/native'
|
} from '@react-navigation/native'
|
||||||
|
|
||||||
import {timeout} from '#/lib/async/timeout'
|
import {timeout} from '#/lib/async/timeout'
|
||||||
|
import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
|
||||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||||
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
@@ -833,11 +834,60 @@ const FlatNavigator = ({
|
|||||||
* to the navigation context.
|
* to the navigation context.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Prevents re-processing the initial deep link URL on remount
|
||||||
|
// (e.g. when account switch changes the key in App.native.tsx)
|
||||||
|
let initialUrlConsumed = false
|
||||||
|
|
||||||
const LINKING = {
|
const LINKING = {
|
||||||
// TODO figure out what we are going to use
|
// TODO figure out what we are going to use
|
||||||
// note: `bluesky://` is what is used in app.config.js
|
// note: `bluesky://` is what is used in app.config.js
|
||||||
prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'],
|
prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'],
|
||||||
|
|
||||||
|
async getInitialURL(): Promise<string | null> {
|
||||||
|
if (initialUrlConsumed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
initialUrlConsumed = true
|
||||||
|
return Linking.getInitialURL()
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribe(listener: (url: string) => void) {
|
||||||
|
const sub = Linking.addEventListener('url', ({url}: {url: string}) => {
|
||||||
|
if (IS_NATIVE && navigationRef.isReady()) {
|
||||||
|
const urlp = parseLinkingUrl(url)
|
||||||
|
const path = urlp.pathname
|
||||||
|
|
||||||
|
// Intent URLs are handled by useIntentHandler
|
||||||
|
if (path.includes('intent/')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [name, params] = router.matchPath(path)
|
||||||
|
|
||||||
|
// For tab roots, switch to the tab
|
||||||
|
if (name === 'Search') {
|
||||||
|
resetToTab('SearchTab')
|
||||||
|
} else if (name === 'Notifications') {
|
||||||
|
resetToTab('NotificationsTab')
|
||||||
|
} else if (name === 'Home') {
|
||||||
|
resetToTab('HomeTab')
|
||||||
|
} else if (name === 'Messages') {
|
||||||
|
resetToTab('MessagesTab')
|
||||||
|
} else {
|
||||||
|
// Navigate (push) instead of resetting state, so that the
|
||||||
|
// native back gesture is properly set up
|
||||||
|
// @ts-ignore nested navigators aren't typed -sfn
|
||||||
|
navigate('HomeTab', {screen: name, params})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web or navigation not ready: use default React Navigation behavior
|
||||||
|
listener(url)
|
||||||
|
})
|
||||||
|
return () => sub.remove()
|
||||||
|
},
|
||||||
|
|
||||||
getPathFromState(state: State) {
|
getPathFromState(state: State) {
|
||||||
// find the current node in the navigation tree
|
// find the current node in the navigation tree
|
||||||
let node = state.routes[state.index || 0]
|
let node = state.routes[state.index || 0]
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ export function getCurrentRoute(state?: State) {
|
|||||||
|
|
||||||
export function isStateAtTabRoot(state: State | undefined) {
|
export function isStateAtTabRoot(state: State | undefined) {
|
||||||
if (!state) {
|
if (!state) {
|
||||||
// NOTE
|
// During initialization (before navigation state is set), default to
|
||||||
// if state is not defined it's because init is occurring
|
// not-at-root. This prevents the drawer gesture from activating during
|
||||||
// and therefore we can safely assume we're at root
|
// the async window before the initial URL is resolved, which would
|
||||||
// -prf
|
// compete with the native stack's back gesture on deep link opens.
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
const currentRoute = getCurrentRoute(state)
|
const currentRoute = getCurrentRoute(state)
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user