npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sendtoapp/react-native

v0.1.14

Published

Deep links for React Native and Expo: one HTTPS link that opens the right screen, with the config plugin that wires up Universal Links and App Links.

Downloads

399

Readme

@sendtoapp/react-native

One HTTPS link that opens the right screen in your app on iOS and Android, and falls back sensibly when the app is not installed.

npm install @sendtoapp/react-native

ESM only. React Native and Expo bundle it without a thought; a Node script written against require() cannot consume it, and needs import or a dynamic await import().

Register a link host at sendtoapp.com first — it is the hostname your links are served from, and the one iOS and Android check against your app.

Expo

Add the config plugin. It writes the Android intent filters and the iOS associated domains entitlement, which is the part people usually get wrong by hand.

// app.config.js
export default {
  plugins: [
    ['@sendtoapp/react-native/app.plugin', { host: 'acme.sendtoapp.com' }]
  ]
};

Then rebuild. Not Expo Go — it cannot verify a domain association. A development build or a release build.

Opening a link

With Expo Router, hand it the redirect hook:

import { createDeepLinkClient, DeepLinkError } from '@sendtoapp/react-native';

const client = createDeepLinkClient({
  host: 'acme.sendtoapp.com',
  // Without this a revoked link, a dead network and a URL that was never yours all arrive as
  // the same silent return to '/'. See "Knowing when a link failed".
  onLinkError: (error, { path }) => {
    // error is typed unknown, because a hook must be able to receive whatever went wrong.
    const reason = error instanceof DeepLinkError ? error.code : error;
    console.warn('[deep-link]', reason, path);
  }
});

export const redirectSystemPath = client.redirectSystemPath;

Anywhere else, subscribe to React Native's Linking:

import { Linking } from 'react-native';
import { createDeepLinkClient } from '@sendtoapp/react-native';

const client = createDeepLinkClient({ host: 'acme.sendtoapp.com' });

const stop = client.subscribe(Linking, route => {
  // '/products/42?color=blue' — hand it to your navigator
});
// Call stop() on cleanup.

subscribe covers both the link tapped while the app is running and the link that launched it, and it never fires twice for the same one. It ignores URLs that are not yours, so it is safe to run alongside your own linking code.

Links that arrive before sign-in

The commonest way an integration fails quietly: a link lands, the screen it points at is behind an auth guard, the navigator cannot mount it, and the app opens on its home screen as though nothing arrived.

createPendingLink parks the route until the app can use it.

// app/+native-intent.js
import { createDeepLinkClient, createPendingLink } from '@sendtoapp/react-native';
import { hasSession } from './lib/session';

const client = createDeepLinkClient({ host: 'acme.sendtoapp.com' });
export const pending = createPendingLink();

export async function redirectSystemPath(input) {
  const route = await client.redirectSystemPath(input);
  if (route !== '/' && !hasSession()) {
    pending.set(route);
    return '/sign-in';
  }
  return route;
}
// wherever a session first exists
const waiting = pending.consume(Boolean(session));
if (waiting) router.replace(waiting);

consume(ready) hands back the route and forgets it, but only when ready is true, so calling it every render before sign-in is safe. peek() reads without consuming and clear() drops a parked route on sign-out. The newest link wins.

Knowing when a link failed

redirectSystemPath returns a route to Expo Router, so it cannot throw. Give it onLinkError and it will tell you why a link did not open instead of quietly sending the user to /.

const client = createDeepLinkClient({
  host: 'acme.sendtoapp.com',
  onLinkError(error) {
    if (!(error instanceof DeepLinkError)) return;
    if (error.code === 'UNAVAILABLE') toast('This invite has expired.');
    else if (error.code === 'TIMEOUT' || error.code === 'NETWORK') toast('Could not reach the network.');
  },
  fallbackRoute: '/'
});

error.code is one of NOT_FOUND, UNAVAILABLE, TIMEOUT, NETWORK, HTTP or INVALID_RESPONSE. subscribe() takes the same handler as its third argument.

resolve() behaves differently on purpose: it returns null for a URL that is not yours, and rejects for one that is yours but could not be resolved. Wrap it in try/catch.

Slow first launches

One attempt gets timeoutMs, 8000 by default. An app's first request also pays for DNS and a TLS handshake, so it is far slower than every request after it — 1.7–2.2s on a wired desktop with DNS cached, once 4.7s with nothing wrong, and several times that on a phone waking its radio. That is exactly when a deep link tends to arrive.

TIMEOUT and NETWORK are retried once; nothing else is, because a 404 answered twice is still a 404 and twice as slow. A dead-but-open network — one that accepts the connection and never answers — is bounded at roughly 16s by the timeout. A network that is plainly down fails in milliseconds and finishes in well under a second.

A NETWORK retry waits 500ms first; a TIMEOUT retry does not, having already spent its budget waiting. A network failure returns in milliseconds — measured at 5ms with the radios off — so retrying it instantly lands while whatever broke is still broken. 500 is a floor measured on an emulator, where a radio came back in about 400ms. Real cellular takes seconds, and this retry will not rescue that.

The wait a user actually experiences is timeoutMs × (retries + 1), because the case that hurts is the one where the first attempt runs out. Measured on a marginal network: the first attempt spent its 8000ms, the retry succeeded in 1.3s, and the link arrived after 9.7 seconds.

createDeepLinkClient({ host: 'acme.sendtoapp.com', timeoutMs: 8000, retries: 1 })

onLinkError reports elapsedMs alongside the error, so the wait is in your logs rather than in a guess. Note what it can and cannot settle: on a TIMEOUT it is the budget you set, not what the request needed — the timeout is what stopped the clock, so it cannot tell you whether a larger value would have landed. The way to find that number is to raise timeoutMs and watch which value stops producing TIMEOUT at all. What elapsedMs does settle is whether the budget was spent: a NETWORK failure at 300ms and a TIMEOUT at 16000 are different problems with different fixes.

onRequest settles the other half, the half none of the numbers above can. Every constant here — 8000, one retry, the 500ms pause — was measured somewhere that is not your users' network, so the package reports on itself rather than asking you to trust them. It fires once per resolution, on success as well as failure:

createDeepLinkClient({
  host: 'acme.sendtoapp.com',
  onRequest: report => console.log('deep_link_request', report)
})

{ attempts: 2, ok: true, pausedMs: 500 } is a retry that rescued a launch. { attempts: 2, ok: false } is one the user waited through for nothing. { attempts: 1, ok: true, elapsedMs: 7400 } says your budget is nearly spent on a network that is working. A week of those answers whether the defaults suit your users, which is a question we cannot answer from here. Anything the hook throws is swallowed; it never changes the route.

report.from says how the link arrived — 'url', 'path' or 'token'. 'path' is the one worth watching: Expo Router is documented to hand redirectSystemPath a bare in-app path as well as a full URL, and on a device it has only ever handed over a URL. The branch that handles the bare path is live code resting on documentation, and your users are the only ones who can settle whether the OS ever takes it.

React Native CLI

The config plugin is Expo-only. For a bare project, declare the intent filter in AndroidManifest.xml and the associated domain in your entitlements yourself — the documentation has both files in full.

Install recovery

If the app was not installed, the visitor goes to the App Store or Google Play. To have the route survive that trip on Android, add @sendtoapp/react-native-referrer or, under Expo, @sendtoapp/expo. Both read the Play Install Referrer, which needs a native module and so cannot live in this package.

Checking it works

A link that opens the browser instead of your app almost always means the association file does not match the installed build — most often the Android fingerprint, where the upload key is used in place of the app signing key. check_association in the MCP server fetches both files and says what is wrong, or read Troubleshooting.

Documentation

Full documentation is at sendtoapp.com/docs, and sendtoapp.com is what the platform does in one page.

Quick start · Expo and React Native · Install recovery · Knowing when a link failed · Troubleshooting · HTTP API

The rest of SendToApp

MIT licensed. Built by SendToApp.