Skip to content

Instantly share code, notes, and snippets.

@shubham43MP
Created December 3, 2025 07:03
Show Gist options
  • Select an option

  • Save shubham43MP/9d5adcf03c45b6073be07a2bd2db93d1 to your computer and use it in GitHub Desktop.

Select an option

Save shubham43MP/9d5adcf03c45b6073be07a2bd2db93d1 to your computer and use it in GitHub Desktop.
// 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;
@shubham43MP
Copy link
Author

shubham43MP commented Dec 3, 2025

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”

  1. Security risk

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.

  1. No fallback

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.

  1. No routing control

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

  1. Universal Links are preferred

From Apple docs:

“Use Universal Links whenever possible. They provide the best experience for users.”

  1. Custom schemes have unreliable behavior

Apple explicitly states:

“If multiple apps declare the same URL scheme, iOS cannot determine which app to open.”

  1. Custom schemes provide no fallback

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.”

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment