The morning after I shipped a "follow us on TikTok" and "share on X" pair of buttons into one of my wallpaper apps, three different reviewers wrote to say nothing happened when they tapped them on iPhone. The buttons worked perfectly in the simulator and on my Development Build, so it took me half a day to narrow it down to one thing: my Info.plist did not declare a single entry under LSApplicationQueriesSchemes.
I have been an indie developer since 2014, mostly shipping wallpaper and well-being apps that I keep alive with AdMob revenue, and Apple's quiet 2015 change to canOpenURL is still one of the most underrated traps in cross-platform development. The fix in Rork (and Expo / React Native in general) lives in your app config, not in your component code.
Recognize the symptoms before chasing the wrong fix
LSApplicationQueriesSchemes problems look like a lot of other Linking bugs, but the signature is specific:
- The buttons work fine in the simulator and in your Development Build, and only break in TestFlight or App Store builds.
canOpenURL("tiktok://...")resolves tofalseeven though TikTok is installed.- Xcode console shows
-canOpenURL: failed for URL: "..." - error: "This app is not allowed to query for scheme tiktok". - The exact same code on Android works correctly.
When you see that fourth bullet — Xcode's explicit "not allowed to query" warning — the diagnosis is done. Since iOS 9, canOpenURL returns false for any scheme you did not declare ahead of time. Apple introduced this as a privacy boundary, because apps were using canOpenURL to silently fingerprint which other apps were installed.
The subtle damage is that openURL itself often still works, but most well-written Rork code path-branches on canOpenURL. The user has TikTok installed, your code thinks they do not, and they end up bounced into Safari every single time. That is a worse experience than not adding the button at all.
Step 1: Add LSApplicationQueriesSchemes to app.json
Rork inherits Expo's app config, so you never have to touch the native Info.plist directly. Edit app.json (or app.config.ts) at your project root:
{
"expo": {
"name": "MyApp",
"ios": {
"bundleIdentifier": "net.dolice.myapp",
"infoPlist": {
"LSApplicationQueriesSchemes": [
"tiktok",
"twitter",
"instagram",
"line",
"fb",
"fb-messenger",
"youtube",
"mailto",
"tel",
"sms"
]
}
}
}
}If your project uses app.config.ts:
import type { ExpoConfig } from "expo/config";
const config: ExpoConfig = {
name: "MyApp",
slug: "myapp",
ios: {
bundleIdentifier: "net.dolice.myapp",
infoPlist: {
LSApplicationQueriesSchemes: [
"tiktok",
"twitter",
"instagram",
"line",
"fb",
"fb-messenger",
"youtube",
"mailto",
"tel",
"sms",
],
},
},
};
export default config;mailto, tel, and sms are technically allowed by default, but I list them anyway because it makes the manifest easier to audit during App Store review prep.
Step 2: Use the right scheme — app name and scheme rarely match
A common stumble: the scheme almost never matches the rebranded app name. To open X (the app formerly known as Twitter), you still need "twitter", not "x". For LINE's "add friend" flow, a Universal Link such as https://line.me/R/ti/p/... is now safer than the line:// scheme.
Reliable scheme reference for the apps that come up most often in Japanese and global indie work:
- TikTok app:
tiktok - X (formerly Twitter):
twitter - Instagram profile:
instagram - LINE "add friend" (prefer Universal Link over
line://) - Facebook page:
fb - Facebook Messenger:
fb-messenger - YouTube:
youtubeorvnd.youtube - Google Maps:
comgooglemaps - Apple Maps:
maps - Spotify:
spotify - Slack:
slack
For anything not on that list, the fastest path is the vendor's own developer docs. Third-party scheme lists tend to be stale, so I always confirm with a real device call to canOpenURL before shipping.
Step 3: Re-run EAS Build — config changes do not hot-reload
This is where most teams lose another half day. Changing app.json does not apply to your existing Development Build, Expo Go session, or TestFlight artifact. infoPlist keys are baked into the native build, so you must rebuild at least once:
# Rebuild the development client
eas build --profile development --platform ios
# Rebuild the production artifact
eas build --profile production --platform ios
# Or local prebuild + direct device install
npx expo prebuild --clean
npx expo run:ios --deviceexpo prebuild --clean regenerates the entire ios/ directory, so back up anything custom you have under ios/ before running it. I personally let CI handle this and just push the app.json change — typically the new iOS build is ready about 15 minutes later.
Step 4: Wrap canOpenURL in a safe fallback
Even with the declaration in place, you still want the calling code to degrade gracefully. Schemes get retired and Apple's behaviour shifts; without a fallback, a silent regression can ship before you notice:
// 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 is unavailable or the scheme is rejected */
webUrl: string;
};
export async function openExternalApp({ appUrl, webUrl }: AppLink): Promise<void> {
try {
const supported = await Linking.canOpenURL(appUrl);
if (supported) {
await Linking.openURL(appUrl);
return;
}
} catch (e) {
// iOS can throw when canOpenURL is called for an undeclared scheme.
// Log it in dev, but always fall back to the web URL in production
// so the user is never stuck on a dead button.
if (__DEV__) {
console.warn("[openExternalApp] canOpenURL threw", e);
}
}
await Linking.openURL(webUrl);
}
// Example
// await openExternalApp({
// appUrl: "tiktok://user?username=dolice",
// webUrl: "https://www.tiktok.com/@masaki.hirokawa",
// });The crucial detail is the outer try/catch. canOpenURL can actually throw on undeclared schemes, and without the catch you will only learn about it from a crash report after release.
Step 5: Know the 50-entry ceiling
Apple caps LSApplicationQueriesSchemes at 50 entries. Going beyond does not get you rejected during review — entries past the 50th are silently ignored. That sounds generous, but if you start covering social, maps, payments, ride-share, and dating apps in the same project, you can blow past the ceiling faster than you expect.
I keep a tiny predeploy check in package.json to catch this before App Store Connect ever sees the build:
// scripts/check-ios-queries.mjs
import fs from "node:fs";
const cfg = JSON.parse(fs.readFileSync("app.json", "utf8"));
const list = cfg?.expo?.ios?.infoPlist?.LSApplicationQueriesSchemes ?? [];
if (list.length > 50) {
console.error(
`LSApplicationQueriesSchemes has ${list.length} entries (Apple limit is 50).`
);
process.exit(1);
}
const dup = list.filter((x, i) => list.indexOf(x) !== i);
if (dup.length > 0) {
console.error(`Duplicate scheme(s) detected: ${dup.join(", ")}`);
process.exit(1);
}
console.log(`OK: ${list.length} scheme(s).`);That predeploy guard has saved me from two near-submission incidents already.
When canOpenURL still returns false, check these three things
If everything above is in place and the result is still false, walk down this list in order:
- Stale build cache. Wipe Xcode's DerivedData, or run
expo prebuild --cleanto regenerateios/. On Rork, rebuild EAS with the cache-clearing flag enabled. - Local network entitlement mix-up. iOS 14's Local Network prompt is a different system from custom URL schemes. Resist the temptation to start touching
NSAppTransportSecurityuntil you confirmLSApplicationQueriesSchemesis correctly declared. - The wrong build is live on App Store Connect. Double-check the build number that TestFlight Internal Testing is actually serving. I once spent half a day debugging a "fixed" app that turned out to be the old binary being promoted untouched.
What to do next
Once Linking.canOpenURL behaves predictably, a lot of growth-oriented experiences become safe to ship — campaign-tracked deep links, conditional onboarding handoffs, app-to-app referrals. Today, open the app.json of your live Rork project and audit every external scheme your buttons can fire. If even one is missing from LSApplicationQueriesSchemes, your iOS users have been silently bouncing to the web all along.
I hope this saves you from the same half-day I spent. Thank you for reading.