◉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)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●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)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●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/Dev Tools
⬡ Dev Tools/2026-05-18Intermediate

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.

Rork585React Native239iOS116LinkingInfo.plist6Troubleshooting43

The morning after I added "follow us on TikTok" and "share on X" buttons to one of my wallpaper apps, three reviewers wrote to say that nothing happened when they tapped them on iPhone. Everything worked in the simulator and in my development build, so narrowing it down to store-distributed builds took half a day.

The cause was embarrassingly small: my Info.plist had no LSApplicationQueriesSchemes entries at all. I had spent those hours suspecting my code, when the answer was in a config file I had not opened.

Before asking whether an app can be opened, declare which apps you are allowed to ask about. That is the whole rule I took from it.

Recognize the symptoms first

Four observations together point strongly at this cause.

What you seeWhat it suggests
Works in the simulator and development build; fails in TestFlight or App Store buildsThe built `Info.plist` differs between builds
`canOpenURL("tiktok://...")` returns `false` (or rejects)The scheme is undeclared
Xcode logs `This app is not allowed to query for scheme tiktok`Close to conclusive
The same code works on AndroidAn iOS-specific rule, though Android 11+ has its own (see below)

React Native's documentation states that as of iOS 9 you must provide LSApplicationQueriesSchemes in Info.plist, or canOpenURL() will always resolve to false. Apple introduced this as a privacy boundary so apps cannot probe which other apps are installed.

openURL itself often still works without a declaration. But if your code branches on canOpenURL to decide between the app and the web, an always-false answer sends everyone to Safari — even people who have TikTok installed.

Fix 1: Add LSApplicationQueriesSchemes in your app config

Rork follows Expo's app config, so you never touch Info.plist directly. In app.json (or app.config.ts):

{
  "expo": {
    "name": "MyApp",
    "ios": {
      "bundleIdentifier": "com.example.myapp",
      "infoPlist": {
        "LSApplicationQueriesSchemes": [
          "tiktok",
          "twitter",
          "instagram",
          "fb",
          "fb-messenger",
          "youtube",
          "comgooglemaps",
          "spotify",
          "slack"
        ]
      }
    }
  }
}

With app.config.ts:

import type { ExpoConfig } from "expo/config";
 
const config: ExpoConfig = {
  name: "MyApp",
  slug: "myapp",
  ios: {
    bundleIdentifier: "com.example.myapp",
    infoPlist: {
      LSApplicationQueriesSchemes: [
        "tiktok",
        "twitter",
        "instagram",
        "fb",
        "fb-messenger",
        "youtube",
        "comgooglemaps",
        "spotify",
        "slack",
      ],
    },
  },
};
 
export default config;

Standard schemes such as http, https, mailto, tel, and sms opened fine for me without a declaration. I used to list them "just in case", but I now keep only what actually needs declaring, which makes later audits easier.

Fix 2: Use the scheme the app really registers

App names and schemes rarely match. For X, writing "x" did not behave as I expected; in my testing "twitter" was the one that worked. That could change on the app's side, so I'd suggest calling canOpenURL on a real device before each release. For LINE's add-friend flow, a Universal Link (https://line.me/R/ti/p/...) is safer than line://.

You want to openSchemeDeclare?
TikTok`tiktok`Yes
X (formerly Twitter)`twitter`Yes
Instagram`instagram`Yes
Facebook / Messenger`fb` / `fb-messenger`Yes
YouTube`youtube` (or `vnd.youtube`)Yes
Google Maps`comgooglemaps`Yes
Spotify / Slack`spotify` / `slack`Yes
LINE add-friendUniversal Link (`https`)No (it is `https`)

For anything else, the vendor's own developer documentation is the safest source. Third-party scheme lists go stale, so I confirm on a device before relying on them.

Fix 3: Rebuild — config changes are baked into the native build

Many people lose a second half-day here. Editing app.json does not affect an existing Expo Go session or Development Build, because infoPlist values are baked into the native binary:

# Rebuild the development client
eas build --profile development --platform ios
 
# Rebuild the production artifact
eas build --profile production --platform ios
 
# Or prebuild locally and install on a device
npx expo prebuild --clean
npx expo run:ios --device

expo prebuild --clean regenerates ios/, so back up anything custom there first.

Fix 4: Don't rely on canOpenURL alone

Even with the declaration in place, a missing fallback means the same symptom returns the day a scheme changes. According to React Native's documentation, canOpenURL can reject on iOS when a scheme is undeclared, and openURL rejects when no registered app can open the URL. So catch both failures and end on the web URL:

// utils/openExternalApp.ts
import { Linking } from "react-native";
 
type AppLink = {
  /** Deep link or Universal Link to the native app */
  appUrl: string;
  /** Web fallback when the app cannot be opened */
  webUrl: string;
};
 
export async function openExternalApp({ appUrl, webUrl }: AppLink): Promise<void> {
  try {
    if (await Linking.canOpenURL(appUrl)) {
      await Linking.openURL(appUrl);
      return;
    }
  } catch (e) {
    // Undeclared scheme (canOpenURL rejects) or no app to open (openURL rejects)
    if (__DEV__) {
      console.warn("[openExternalApp] fallback to web", e);
    }
  }
  await Linking.openURL(webUrl);
}
 
// Example
// await openExternalApp({
//   appUrl: "tiktok://user?username=example",
//   webUrl: "https://www.tiktok.com/@example",
// });

The outer try/catch is the point. Keep in mind that this fallback can also hide a configuration gap — I'd always log a warning in development builds so a quiet drift to the web does not go unnoticed in production.

Fix 5: Call limits, and Android's separate rule

React Native's documentation says that for apps linked against an earlier iOS but running on iOS 9 or later, canOpenURL can be called up to 50 times; after that it always resolves to false, until the app is reinstalled or upgraded. I could not confirm Apple's own page on how many schemes you may declare, so I won't state a hard cap here. Still, calling canOpenURL every time a button renders is wasteful; checking once at startup and caching the result has been easier for me to reason about.

On the Android side, when you target Android 11 (SDK 30) or later, the same documentation says you must declare the intents in AndroidManifest.xml under <queries>. So "it works on Android" may not hold for every target SDK.

If canOpenURL still returns false

Walk through these in order.

  1. Wrong build. Check that the build number served by TestFlight matches your code. I once re-submitted an old binary after fixing app.json and lost half a day.
  2. EAS cache. Re-run eas build with --clear-cache. EAS builds in the cloud, so clearing your local Xcode DerivedData will not help.
  3. Spelling. Compare the declared string and the URL you call, including case and the :// part.

What to do next

Today, open your Rork project's app.json and check each external scheme your buttons can fire against LSApplicationQueriesSchemes, one by one. If even one is missing, your iOS users may have been quietly landing on the web all along.

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-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ Dev Tools2026-07-27
When to Raise Your Minimum iOS Version — Count Leftover Branches, Not User Percentages
Judging a minimum OS bump by usage share produces the same answer every year, so the decision never happens. Here is the annotation convention, the sweep script that counts how many iOS and Android branches each candidate floor would retire, and what to watch for 30 days after.
⬡ Dev Tools2026-05-28
Tracking Down BGTaskScheduler.submit Error Code=1 (Unavailable) in Rork iOS Apps
When BGTaskScheduler.submit returns Error Code=1, the cause space is finite — six of them. Covers telling Code=1 from Code=3, the identifier trap in Expo-based Rork apps, submitting from a widget extension by mistake, and a getPendingTaskRequests self-check.