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:
Claude
2026-03-09 13:11:36 +00:00
parent 18d7e775f6
commit d87838f617
2 changed files with 55 additions and 5 deletions
+50
View File
@@ -18,6 +18,7 @@ import {
} from '@react-navigation/native'
import {timeout} from '#/lib/async/timeout'
import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
@@ -833,11 +834,60 @@ const FlatNavigator = ({
* 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 = {
// TODO figure out what we are going to use
// note: `bluesky://` is what is used in app.config.js
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) {
// find the current node in the navigation tree
let node = state.routes[state.index || 0]
+5 -5
View File
@@ -25,11 +25,11 @@ export function getCurrentRoute(state?: State) {
export function isStateAtTabRoot(state: State | undefined) {
if (!state) {
// NOTE
// if state is not defined it's because init is occurring
// and therefore we can safely assume we're at root
// -prf
return true
// During initialization (before navigation state is set), default to
// not-at-root. This prevents the drawer gesture from activating during
// the async window before the initial URL is resolved, which would
// compete with the native stack's back gesture on deep link opens.
return false
}
const currentRoute = getCurrentRoute(state)
return (