In a bare React Native app, how do you register a custom URL scheme like resetapp:// on iOS and Android?
answer
- native config, not JavaScript
- iOS: CFBundleURLTypes in Info.plist
- Android: VIEW + DEFAULT + BROWSABLE
- a separate intent-filter on MainActivity
- rebuild the binary to apply
basics
~20 sRegister the scheme natively: on iOS add it under CFBundleURLTypes / CFBundleURLSchemes in Info.plist; on Android add an intent-filter with the VIEW action, DEFAULT and BROWSABLE categories and a data scheme to MainActivity. Then rebuild the app.
solid answer
~40 sA scheme is claimed by the native app, not by JavaScript. On iOS I add a `CFBundleURLTypes` entry to `Info.plist` whose `CFBundleURLSchemes` array contains `resetapp`. On Android I add a new `<intent-filter>` to `MainActivity` in `AndroidManifest.xml` with the `android.intent.action.VIEW` action, the `DEFAULT` and `BROWSABLE` categories — `BROWSABLE` lets a browser link launch it — and `<data android:scheme="resetapp" />`; it must be its own filter, not added to the `MAIN`/`LAUNCHER` one. Both are native changes, so I rebuild and reinstall; a JavaScript reload or an over-the-air update does not apply them. Then I test with a real link, for example `npx uri-scheme open resetapp://reset-password --ios`. Handing the incoming URL to JavaScript on iOS also needs the `RCTLinkingManager` forwarding in the app delegate.
code
xml · 11 lines<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.example.resetapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>resetapp</string>
</array>
</dict>
</array>go deeper
Remember that schemes are registered natively: CFBundleURLTypes in Info.plist on iOS and an intent filter in AndroidManifest.xml on Android.
Explain each part of the Android filter, why it must be separate from the launcher filter, and why a rebuild rather than a reload is required.
Choose a collision-resistant scheme, keep the number of schemes small, and remember any app can fire the scheme with arbitrary parameters.
Decide which entry points deserve a custom scheme at all versus verified https links, weighing install state, security and support cost.
## Who owns a scheme A **custom URL scheme** such as `resetapp://` is a promise the operating system keeps: "when something opens a URL with this scheme, launch this app". The OS learns it from the app's **native configuration**, read at install time. Nothing in JavaScript — not `Linking`, not a navigation library's `prefixes` — registers a scheme. So registration is two native edits, one per platform, followed by a rebuild. ## iOS: CFBundleURLTypes In `ios/<App>/Info.plist`, add a `CFBundleURLTypes` array containing a dictionary whose `CFBundleURLSchemes` array lists the scheme: - `CFBundleURLTypes` — the list of URL types the app handles. - `CFBundleURLSchemes` — the schemes for one URL type, without `://`. - An optional `CFBundleURLName` identifies the URL type, commonly in reverse-DNS form. Xcode shows the same data under the target's **Info → URL Types**. For the URL to reach JavaScript, the app delegate forwards `application:openURL:options:` to `RCTLinkingManager`, as React Native's Linking documentation shows; how that URL is then received on cold and warm start is a separate topic. ## Android: an intent filter on MainActivity In `android/app/src/main/AndroidManifest.xml`, add an `<intent-filter>` inside the `MainActivity` `<activity>`: 1. `<action android:name="android.intent.action.VIEW" />` — the activity can display data given to it. 2. `<category android:name="android.intent.category.DEFAULT" />` — it can receive implicit intents. 3. `<category android:name="android.intent.category.BROWSABLE" />` — a browser may launch it from a tapped link. 4. `<data android:scheme="resetapp" />` — the scheme it claims (optionally with `android:host` to narrow it). Put it in a **separate** `<intent-filter>`: an intent filter matches as a unit, and the existing `MAIN`/`LAUNCHER` filter is what puts the app icon on the home screen. Expo's own manifest tooling follows the same rule — it recognises a link filter as one with the `VIEW` action and no `LAUNCHER` category, placed on the `singleTask` activity. ## Apply and test | Step | iOS | Android | |---|---|---| | Rebuild | rebuild and reinstall from Xcode or the CLI | rebuild and reinstall the APK | | Test | `npx uri-scheme open resetapp://reset-password --ios` | `npx uri-scheme open resetapp://reset-password --android` | | Also try | tap the link in Safari or Notes | tap the link in a browser page or chat | Neither a Metro reload nor an over-the-air JavaScript update can add a scheme: they change the JavaScript bundle, while the scheme lives in the installed binary's configuration. ## Choosing the scheme string - Use lowercase letters, and avoid generic words (`app`, `reset`, `myapp`) that another app might also claim. - A name derived from your bundle identifier is less likely to collide, but **nothing makes it unique**: any app can declare the same scheme. - Keep one scheme per app where possible; each extra scheme is another entry point to validate. ## What registration does not give you - It does not work when the app is **not installed**: the link simply fails, or the browser shows an error. Verified `https` links (Universal Links and App Links) fall back to the website instead. - It does not make links **trustworthy**. Any web page or app can open `resetapp://` with any parameters, so the screen it reaches must validate them. - It does not route the URL to a screen. Mapping `reset-password?token=…` onto a navigator's screen is the navigation library's linking configuration. ## What a strong answer shows It places the work in native config, names `CFBundleURLTypes`/`CFBundleURLSchemes` and the `VIEW`/`DEFAULT`/`BROWSABLE` filter with a `data` scheme, insists on a separate filter and a rebuild, and shows how to test the link from outside the app.
- Why must the VIEW filter be separate from the MAIN/LAUNCHER filter on Android?Android matches an intent against each filter as a whole. The `MAIN`/`LAUNCHER` filter is what puts the app on the home screen; mixing the link's action, categories and data into it changes what that filter matches instead of adding a new entry point. A separate `VIEW`/`BROWSABLE` filter keeps both behaviours.
- You added the scheme and reloaded Metro, but tapping resetapp:// still does nothing. Why?The scheme lives in `Info.plist` and `AndroidManifest.xml`, which the OS reads from the installed binary. A Metro reload only replaces the JavaScript bundle, so the old binary still does not claim the scheme. Rebuild and reinstall the app, then test again.
saying these in an interview costs you the question
- Adding the scheme to the navigation library's prefixes registers it with the OS.
- A Metro reload or an over-the-air update is enough after adding a scheme.
- The data element can go inside the existing MAIN/LAUNCHER intent filter.
- Only the VIEW action is needed; BROWSABLE makes no difference for links in a browser.
- Choosing an unusual scheme name guarantees no other app can claim it.