Created
December 3, 2025 07:03
-
-
Save shubham43MP/9d5adcf03c45b6073be07a2bd2db93d1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // useDeepLinker.ts | |
| import { useCallback, useEffect, useRef } from "react"; | |
| export type DeepLinkOptions = { | |
| onIgnored?: () => void; | |
| onFallback?: () => void; | |
| onReturn?: () => void; | |
| /** | |
| * Time to wait before deciding the browser "ignored" the deep link. | |
| * Default: 500ms (same as original code) | |
| */ | |
| dialogTimeoutMs?: number; | |
| }; | |
| export type UseDeepLinkerResult = { | |
| openUrl: (url: string) => void; | |
| }; | |
| /** | |
| * React/TS version of the DeepLinker logic using useEffect and hooks. | |
| * | |
| * Handles: | |
| * - onIgnored: browser never popped a dialog / deep link did nothing | |
| * - onFallback: dialog closed / user returned but deep link failed | |
| * - onReturn: user came back from native app after a successful open | |
| */ | |
| export function useDeepLinker(options: DeepLinkOptions): UseDeepLinkerResult { | |
| const hasFocusRef = useRef(true); | |
| const didHideRef = useRef(false); | |
| const optionsRef = useRef<DeepLinkOptions>(options); | |
| // Always keep the latest callbacks without re-binding listeners | |
| useEffect(() => { | |
| optionsRef.current = options; | |
| }, [options]); | |
| useEffect(() => { | |
| if (typeof window === "undefined" || typeof document === "undefined") { | |
| return; | |
| } | |
| const onBlur = () => { | |
| hasFocusRef.current = false; | |
| }; | |
| const onVisibilityChange = (e: Event) => { | |
| const target = e.target as Document | null; | |
| if (target && target.visibilityState === "hidden") { | |
| didHideRef.current = true; | |
| } | |
| }; | |
| const onFocus = () => { | |
| const { onFallback, onReturn } = optionsRef.current; | |
| if (didHideRef.current) { | |
| // User returned from native app or browser came back into view | |
| if (onReturn) { | |
| onReturn(); | |
| } | |
| didHideRef.current = false; // reset | |
| } else { | |
| // Ignore duplicate focus when returning from native app on iOS 13.3+ | |
| if (!hasFocusRef.current && onFallback) { | |
| // Wait for app switch transition to fully complete – only then | |
| // does 'visibilitychange' reliably fire | |
| window.setTimeout(() => { | |
| // If browser was not hidden, the deep link failed | |
| if (!didHideRef.current) { | |
| onFallback(); | |
| } | |
| }, 1000); | |
| } | |
| } | |
| hasFocusRef.current = true; | |
| }; | |
| const bindEvents = (mode: "add" | "remove") => { | |
| const handler = | |
| mode === "add" ? "addEventListener" : ("removeEventListener" as const); | |
| window[handler]("blur", onBlur); | |
| window[handler]("focus", onFocus); | |
| document[handler]("visibilitychange", onVisibilityChange); | |
| }; | |
| // Attach listeners | |
| bindEvents("add"); | |
| // Cleanup on unmount | |
| return () => { | |
| bindEvents("remove"); | |
| }; | |
| }, []); | |
| const openUrl = useCallback((url: string) => { | |
| if (typeof window === "undefined") return; | |
| const { onIgnored, dialogTimeoutMs = 500 } = optionsRef.current; | |
| // It can take a while for the native dialog to appear | |
| window.setTimeout(() => { | |
| if (hasFocusRef.current && onIgnored) { | |
| // Browser failed to respond to the deep link | |
| onIgnored(); | |
| } | |
| }, dialogTimeoutMs); | |
| // Trigger the deep link | |
| window.location.href = url; | |
| }, []); | |
| return { openUrl }; | |
| } | |
| // usage | |
| import React, { useEffect } from "react"; | |
| const DeepLinkEffectExample: React.FC = () => { | |
| const { openUrl } = useDeepLinker({ | |
| onIgnored: () => { | |
| console.log("Browser ignored the deep link (no dialog, no redirect)"); | |
| }, | |
| onFallback: () => { | |
| console.log("Deep link failed; user returned to tab. Trigger fallback."); | |
| }, | |
| onReturn: () => { | |
| console.log("User returned from native app"); | |
| }, | |
| }); | |
| // Example boolean condition | |
| const shouldTriggerDeepLink = true; // Replace with real logic | |
| useEffect(() => { | |
| if (!shouldTriggerDeepLink) return; | |
| // Deep link you want to open | |
| const APP_URL = "fb://profile/240995729348595"; | |
| // Trigger the link | |
| openUrl(APP_URL); | |
| }, [shouldTriggerDeepLink, openUrl]); | |
| return <div>Attempting deep link…</div>; | |
| }; | |
| export default DeepLinkEffectExample; |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
One of the things to note is Apple Prefers to use Universal Links over Custom Scheme and that is very important to note because the above issue stems from this fact.
Apple didn’t suddenly prefer Universal Links.
They intentionally shifted the entire iOS deep-linking ecosystem away from custom URL schemes starting in iOS 9, and have been tightening the rules every single year.
Below is the timeline, official documentation, and the exact reasons Apple did this.
✅ Timeline: When Apple moved from Custom Schemes → Universal Links
iOS 9 (2015) — Universal Links introduced
Apple publicly introduced Universal Links at WWDC 2015.
Official announcement:
https://developer.apple.com/videos/play/wwdc2015/509/
In this session, Apple tells developers:
Custom URL schemes cannot guarantee that the right app opens
Custom schemes allow URL hijacking (any app can register your scheme)
Universal Links are safer, more predictable, and controlled by domain ownership
This is the moment Apple began strong preference for Universal Links.
🧨 Why custom schemes became “deprecated in practice”
Anyone can register:
fb://
bankapp://
So:
Wrong app might intercept the link
Malicious apps can hijack financial deep links
No way for iOS to verify ownership
Apple hates this.
If the app is not installed:
Nothing happens
No UI
JS doesn’t know whether it failed
User experience is inconsistent
Exactly the problems you're fighting with.
You can't map:
bankapp://onboarding/step1
to a webpage automatically.
With Universal Links, you can.
💡 Apple’s official position (straight from docs)
Here is the clear documentation language from Apple:
Apple Developer Docs — “Allowing apps and websites to link to your content”
https://developer.apple.com/documentation/xcode/supporting-associated-domains
“Universal Links are the preferred method for iOS to open your app from a link.”
More:
“Avoid using custom URL schemes for deep linking as they do not provide the user experience that Universal Links offer.”
📌 Key official statements from Apple
From Apple docs:
“Use Universal Links whenever possible. They provide the best experience for users.”
Apple explicitly states:
“If multiple apps declare the same URL scheme, iOS cannot determine which app to open.”
From the Universal Links WWDC session:
“If the app is not installed, nothing happens. This is not ideal for users.”
📅 Post-iOS 9 changes (confirmation of preference)
iOS 11–12
Safari began suppressing automatic redirects more aggressively.
JS timers continued running after app open → race conditions (your issue).
Apple hardened the deep linking pipeline.
iOS 13+
universal links became the only guaranteed way to launch an app from Safari without user confirmation.
Custom schemes became increasingly unreliable in:
Safari
Safari View Controller
In-app browsers (Mail, LinkedIn, Facebook, etc.)
iOS 14+
Private Relay, Intelligent Tracking Prevention, site isolation → further damaged JS-based fallback detection for custom schemes.
iOS 16–17
Universal Links became the only supported deep-link method in PWAs and SFSafariViewController.
Custom schemes still work, but with unpredictable UI, timing, and event behavior.
🧩 Why you are seeing the race condition issue in Safari
Because Apple has spent the last 9 years intentionally reducing the reliability of:
JS timers
focus/blur
visibilitychange
backgrounding events
after initiating a custom-scheme deep link.
This is by design.
Apple's goal:
Push all financial institutions, banks, and secure apps to use Universal Links, not custom schemes.
🔥 Summary (exact answer to your question)
When did Apple start preferring Universal Links?
iOS 9 (2015) — Universal Links introduced and described as “preferred”.
Is there official documentation showing preference?
Yes:
Apple Associated Domains Documentation
https://developer.apple.com/documentation/xcode/supporting-associated-domains
Apple Universal Links Guide
https://developer.apple.com/ios/universal-links/
WWDC 2015 – Introducing Universal Links (session 509)
https://developer.apple.com/videos/play/wwdc2015/509/
Apple Docs: Avoid custom URL schemes
(same docs as above)
What does Apple say?
“Universal Links are the preferred method.”
“Avoid custom URL schemes due to their limitations.”
“Use Universal Links whenever possible.”