@@ -155,7 +155,10 @@ function StackNavigator() {
155155 } ) ;
156156 } ;
157157
158- const dispatchDeepLink = async ( incomingUrl ?: string | null ) => {
158+ const dispatchDeepLink = async (
159+ incomingUrl ?: string | null ,
160+ opts : { silentFailure ?: boolean } = { } ,
161+ ) : Promise < boolean > => {
159162 const magnetLink = extractMagnetLink ( incomingUrl ) ;
160163 if ( magnetLink ) {
161164 const now = Date . now ( ) ;
@@ -164,28 +167,28 @@ function StackNavigator() {
164167 lastHandledMagnetRef . current . value === magnetLink &&
165168 now - lastHandledMagnetRef . current . at < 1500
166169 ) {
167- return ;
170+ return true ;
168171 }
169172 lastHandledMagnetRef . current = { value : magnetLink , at : now } ;
170173
171174 if ( ! rootNavReadyRef . current ) {
172175 pendingDeepLinkRef . current = { type : 'magnet' , value : magnetLink } ;
173- return ;
176+ return true ;
174177 }
175178 navigateToMagnet ( magnetLink ) ;
176- return ;
179+ return true ;
177180 }
178181
179182 const rawTorrentFile = extractTorrentFile ( incomingUrl ) ;
180- if ( ! rawTorrentFile ) return ;
183+ if ( ! rawTorrentFile ) return false ;
181184
182185 const now = Date . now ( ) ;
183186 if (
184187 lastHandledTorrentFileRef . current &&
185188 lastHandledTorrentFileRef . current . value === rawTorrentFile . uri &&
186189 now - lastHandledTorrentFileRef . current . at < 1500
187190 ) {
188- return ;
191+ return true ;
189192 }
190193 lastHandledTorrentFileRef . current = { value : rawTorrentFile . uri , at : now } ;
191194
@@ -201,15 +204,18 @@ function StackNavigator() {
201204 const torrentFile = await persistIncomingTorrentFile ( rawTorrentFile ) ;
202205 if ( ! torrentFile ) {
203206 clogWarn ( 'LINK' , `Could not read incoming .torrent: ${ rawTorrentFile . uri } ` ) ;
204- showToast ( t ( 'errors.couldNotReadTorrentFile' ) , 'error' ) ;
205- return ;
207+ if ( ! opts . silentFailure ) {
208+ showToast ( t ( 'errors.couldNotReadTorrentFile' ) , 'error' ) ;
209+ }
210+ return false ;
206211 }
207212
208213 if ( ! rootNavReadyRef . current ) {
209214 pendingDeepLinkRef . current = { type : 'torrentFile' , value : torrentFile } ;
210- return ;
215+ return true ;
211216 }
212217 navigateToTorrentFile ( torrentFile ) ;
218+ return true ;
213219 } ;
214220
215221 const subscription = Linking . addEventListener ( 'url' , ( { url } ) => {
@@ -222,26 +228,35 @@ function StackNavigator() {
222228 // Two independent sources for the cold-launch URL, because neither is
223229 // reliable alone on iOS here:
224230 // - expo-linking's getLinkingURL() reads a native registry populated
225- // by the "open url" AppDelegate callback (the one our
226- // withNativeTorrentFileCopy patch rewrites to an app-owned copy).
231+ // by the "open url" AppDelegate callback, which can still carry the
232+ // original security-scoped file:// URI whose access has already
233+ // lapsed by the time JS reads it.
227234 // - RN core's Linking.getInitialURL() reads bridge.launchOptions,
228- // which the New Architecture may not populate.
229- // Try both: dispatchDeepLink de-dupes by URL, so whichever arrives
230- // first wins and the other is a no-op. Do NOT reduce this to one
235+ // which our withNativeTorrentFileCopy patch rewrites to an
236+ // app-owned copy of an incoming .torrent — but the New Architecture
237+ // may not populate it at all.
238+ // Try the RN source first and only fall back to expo-linking's URL —
239+ // silently — when the two disagree, so a stale/expired scoped URI from
240+ // one source doesn't flash a failure toast right before the other
241+ // source's valid copy succeeds (#175). Do NOT reduce this to one
231242 // source without testing a real cold launch — cold-launch "Open In"
232243 // has regressed repeatedly on exactly this code path.
233- const expoUrl = ExpoLinking . getLinkingURL ( ) ;
234- clogInfo ( 'LINK' , `Cold-launch expo-linking URL: ${ expoUrl ?? '(none)' } ` ) ;
235- void dispatchDeepLink ( expoUrl ) ;
244+ void ( async ( ) => {
245+ const [ expoUrl , rnUrl ] = await Promise . all ( [
246+ Promise . resolve ( ExpoLinking . getLinkingURL ( ) ) ,
247+ Linking . getInitialURL ( ) . catch ( ( ) => null ) ,
248+ ] ) ;
249+ clogInfo ( 'LINK' , `Cold-launch expo-linking URL: ${ expoUrl ?? '(none)' } ` ) ;
250+ clogInfo ( 'LINK' , `Cold-launch RN URL: ${ rnUrl ?? '(none)' } ` ) ;
236251
237- Linking . getInitialURL ( )
238- . then ( ( rnUrl ) => {
239- clogInfo ( 'LINK' , `Cold-launch RN URL: ${ rnUrl ?? '(none)' } ` ) ;
240- return dispatchDeepLink ( rnUrl ) ;
241- } )
242- . catch ( ( ) => {
243- // No initial URL — safe to ignore.
244- } ) ;
252+ const hasDistinctFallback = ! ! expoUrl && expoUrl !== rnUrl ;
253+ const handled = rnUrl
254+ ? await dispatchDeepLink ( rnUrl , { silentFailure : hasDistinctFallback } )
255+ : false ;
256+ if ( ! handled && hasDistinctFallback ) {
257+ await dispatchDeepLink ( expoUrl ) ;
258+ }
259+ } ) ( ) ;
245260 }
246261
247262 if ( pendingDeepLinkRef . current ) {
0 commit comments