Skip to content

Repository files navigation

@uselinking/react-native

Deferred deep linking for React Native / Expo.

When someone taps your link without the app installed, they go to the App Store or Play Store — and the link's intent is lost. This SDK carries it across that gap: on the first launch after install, your app receives the link that was tapped and can open the right screen.

You bring the server. The SDK is MIT-licensed and speaks one HTTP endpoint, POST {endpoint}/v1/match, with an opaque bearer token. Point it at your own backend — PROTOCOL.md specifies everything a server has to do, and ships with a conformance test you can run against your deployment.

The hosted service at api.uselinking.com is currently offline. endpoint has no default: pass the origin of a server you run.

Install

npx expo install @uselinking/react-native @react-native-async-storage/async-storage

Add the config plugin to app.json / app.config.js with your link domain — it sets up the iOS Associated Domains entitlement and the Android autoVerify intent filter, which is the step most integrations get wrong:

{
  "expo": {
    "plugins": [
      ["@uselinking/react-native", { "linkDomain": "links.example.com" }]
    ]
  }
}

linkDomain is the host your server serves short links and the domain-association files from. This changes native config, so rebuild the app (expo prebuild + a native build, or an EAS build) — an OTA update is not enough.

Use

import { DeepLinkProvider, useDeferredLink } from '@uselinking/react-native';

export default function App() {
  return (
    <DeepLinkProvider
      config={{
        endpoint: 'https://api.example.com', // your server — required
        apiKey: process.env.EXPO_PUBLIC_LINK_KEY,
      }}
    >
      <Root />
    </DeepLinkProvider>
  );
}

function Root() {
  const { isLinkProcessed, link, clearLink } = useDeferredLink();

  useEffect(() => {
    if (!isLinkProcessed || !link) return;
    navigate(link.path, link.params); // e.g. "/events/join", { it: "abc123" }
    clearLink();
  }, [isLinkProcessed, link]);
}

Imperative API, for gating startup on the match:

import { init, getInitialLink, waitForInitialLink } from '@uselinking/react-native';

init({ endpoint: 'https://api.example.com', apiKey: '…' });
await waitForInitialLink(5000); // resolves when the attempt settles, or times out
const link = await getInitialLink(); // null on later launches

Configuration

interface Config {
  endpoint: string;        // origin of your server. Required, no default
  apiKey: string;          // sent as `Authorization: Bearer <apiKey>`; any format
  appId?: string;          // reserved; not sent on the wire yet
  matchTimeoutMs?: number; // per-request budget for /v1/match. Default 5000
}

init() throws if endpoint is missing. That is deliberate: without it every match would quietly return nothing, which looks exactly like "no link was tapped" — a dead feature you would not notice until someone reported it.

Pointing at a dev server: http://localhost:3000 works from the iOS Simulator, and the Android emulator reaches the host through http://10.0.2.2:3000.

Running your own server

Your server has three jobs. PROTOCOL.md specifies each one in detail, including the reference matching algorithm:

  1. Capture clicks on your link domain. A tap on https://<linkDomain>/<code> records a pending click — the deep path and params, plus enough about the device to recognise it later — and redirects to the App Store or Play Store.

  2. Serve domain-association files on that same domain (/.well-known/apple-app-site-association and /.well-known/assetlinks.json) so the link opens the app directly once it is installed.

  3. Answer POST /v1/match, the only endpoint this SDK calls. On the first launch after install it sends the device's platform, OS version, model, locale and timezone — plus the Play Install Referrer on Android — and your server decides whether a pending click belongs to this install:

    { "matchType": "unique",
      "link": { "url": "", "path": "/invite/9fb2", "params": {}, "clickId": "" } }

Android is deterministic: the click id travels through the Play Store referrer. iOS has no equivalent, so matching is probabilistic (IP bucket + locale + device class + OS major) and must fail open — return none rather than risk opening the wrong screen.

test/live-api.e2e.test.ts is the conformance test. Point E2E_ENDPOINT, E2E_SHORT_URL and E2E_API_KEY at your deployment and it drives a real click through to a real match, including the consume-once check.

Match confidence

link.matchType tells you how sure the server is, so you can decide what to trust:

value meaning
unique Exactly one pending click matched every signal. Safe to act on.
weak Only one click was pending, but a signal disagreed (e.g. locale). Probably right.
No confident match: link is null and the app does its normal first launch.

Notes

  • One match attempt per install, on the first launch. Later launches resolve from local storage immediately.
  • The response is persisted before it is delivered, so a crash during startup doesn't lose the link.
  • Network failures settle as "no link" and retry on the next launch — the SDK never blocks or breaks app startup.
  • No cross-app tracking, no advertising identifiers. Signals are used once to match your own link, then the pending click is consumed.

License

MIT

About

Deferred deep linking for React Native & Expo — carry a link's intent across the app store install. Client SDK for uselinking.com

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages