◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 35 days left. If your app calls it, check the migration target●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 35 days left. If your app calls it, check the migration target●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/App Dev
◇ App Dev/2026-10-26Intermediate

Linking.canOpenURL Keeps Returning false on iOS Only — Declaring LSApplicationQueriesSchemes, and the 25-Entry Cap Coming With iOS 27

When Linking.canOpenURL returns false on iOS no matter what is installed, the cause is usually a missing Info.plist declaration. Here is how to fix it in Expo's app.json, what Android needs instead, and how to prepare for the 25-entry limit for apps linked on iOS 27.

Expo228iOS117LinkingcanOpenURLInfo.plist8Troubleshooting49

One evening I added an "Open in LINE" button to an app. On Android it worked as intended: if the app was installed it opened, otherwise it sent people to the store. On iOS, in the simulator and on a real device, Linking.canOpenURL returned false every time, so everyone landed on the store page. LINE was definitely installed.

I spent about an hour suspecting my code. The cause was outside the code: I had never declared the scheme I wanted to check in Info.plist.

The thing I'd like to say first is that canOpenURL returning false and openURL failing are two different problems. Keeping them apart makes the diagnosis much shorter.

Two causes cover almost every case

Apple's documentation says that for apps linked on or after iOS 9.0, any scheme you pass to canOpenURL must be listed under LSApplicationQueriesSchemes in Info.plist. An undeclared scheme always returns false, even when a matching app is installed.

The other cause is simply that no app on the device handles the scheme. That also returns false, and from the outside the two look identical.

Here is the order I check things in:

StepWhat to checkWhat it tells you
1Is the scheme in Info.plist?If not, it is a missing declaration
2Spelling, and no "://" in the entryOnly the scheme name goes in the plist
3Did you re-run prebuild?Editing app.json alone may not reach the native project
4Are you testing in Expo Go?Expo Go uses its own Info.plist, so your declaration has no effect

Step 4 is where I got stuck first. As long as you test in Expo Go, nothing you put in app.json changes the result. You need a development build.

The app.json setup in Expo

Expo's guide shows LSApplicationQueriesSchemes under ios.infoPlist:

{
  "expo": {
    "ios": {
      "infoPlist": {
        "LSApplicationQueriesSchemes": ["line", "twitter"]
      }
    }
  }
}

This tells the OS which schemes you intend to query. It is line, not line://, because only the scheme name belongs in the plist.

In app code, I keep the check and the navigation separate so each is easy to change later:

import { Linking } from 'react-native';
 
async function openOrFallback(appUrl: string, storeUrl: string) {
  try {
    const installed = await Linking.canOpenURL(appUrl);
    await Linking.openURL(installed ? appUrl : storeUrl);
  } catch (e) {
    // On Android the check itself can reject
    await Linking.openURL(storeUrl);
  }
}

The try/catch is there because Expo's docs note that the promise rejects on Android when the check can't be made. I'd rather send people to the store than leave the screen stuck.

Android 11 and later: a different place to declare

Android has no Info.plist. Package visibility is handled by queries in AndroidManifest. Expo's guide adds manifest.queries through a config plugin rather than app.json, so copying the iOS habit of editing app.json won't change anything on Android.

iOS 27 lowers the limit to 25 entries

This is the part I most wanted to write down. According to Apple's documentation, apps linked on or after iOS 15 can list up to 50 entries in LSApplicationQueriesSchemes, and apps linked on or after iOS 27 are limited to 25.

If your app checks dozens of share targets, rebuilding with Xcode 27 could leave the later entries without effect. I'd handle it in three ways:

  • Keep only the schemes you truly need to check (for share targets, a short list of the most common ones).
  • Where you don't need to check, skip canOpenURL and call openURL directly — it isn't bound by the declaration.
  • Move toward universal links where you can, since the browser opens when no app is installed.

In my own wallpaper apps, I'm going through each place that checks before branching and keeping only the ones that matter.

A note on review

Listing a long run of schemes just to probe for other apps may prompt questions about purpose during review. I'd keep the list to links your screens genuinely use. If a build stalls in review, this checklist for blank-screen rejections may help.

When you touch native iOS configuration, prebuild behavior matters too. If you hit AppDelegate errors, see this note on the prebuild AppDelegate error. For the Android side, auditing dependency manifests is a useful companion.

Check as little as possible, and navigate with openURL. I try to hold to that line even as the list of schemes grows.

One next step: count how many schemes in your app.json you actually pass to canOpenURL. If the number is 25 or fewer, there's nothing to rush.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-05-18
Fixing Linking.canOpenURL False Returns on iOS in Rork
Production iOS builds silently route every user to the mobile web instead of opening TikTok or X? This is almost always a missing LSApplicationQueriesSchemes entry. Here is the full fix path for Rork.
◇ App Dev2026-10-18
It Ran Fine in Debug, but the Release Build Died on Launch with a Resource Not Found. A File I Looked Up by Name Had Been Shrunk Away
A release-only crash at launch, caused by images and sounds looked up by a name string being removed by resource shrinking. A keep.xml written at prebuild, plus a post-build check that confirms it worked.
◇ App Dev2026-10-15
When adjustsFontSizeToFit Shrinks Just One Row, Suspect the Box Height, Not the String
Only the 10th row of a list drops to 4pt on iOS, though the data and strings are fine. I read the Yoga rounding and font-fit sources, then counted how often the box comes up short in Node.