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

@linkforty/mobile-sdk-expo

v1.10.0

Published

Expo SDK for LinkForty - open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links. Deferred deep linking, attribution, and smart routing.

Readme

LinkForty Expo SDK

Expo SDK for LinkForty — the open-source alternative to Branch.io, AppsFlyer OneLink, and Firebase Dynamic Links. Add deferred deep linking, mobile attribution, and smart link routing to your Expo app. Self-hosted, privacy-first, no per-click pricing. No config plugin: the SDK is JavaScript on top of Expo modules.

npm version License: MIT

Features

  • Deferred deep linking (install attribution: exact from the Google Play install referrer on Android, otherwise by device fingerprinting)
  • Direct deep linking with server-side URL resolution
  • Event tracking with an offline queue (persists across app restarts)
  • Revenue tracking
  • Automatic screen tracking with React Navigation (opt-in)
  • Programmatic link creation
  • No config plugin: built on expo-device, expo-application, expo-localization, expo-linking and AsyncStorage

Requirements

  • Expo SDK 51 or later (built and tested against Expo SDK 58), React Native 0.74 or later, React 18 or later
  • The Expo modules the SDK runs on, installed in your app: expo-device, expo-application, expo-localization, expo-linking and @react-native-async-storage/async-storage 1.23.1 or later 2.x (AsyncStorage 3 is not supported). They are peer dependencies, so your app's versions are the ones used
  • A development build or EAS build to test links: Expo Go can't claim your link domains, so Universal Links and App Links won't open your app there
  • Optional: @react-navigation/native 6 or later, only for automatic screen tracking

Installation

npx expo install @linkforty/mobile-sdk-expo expo-device expo-application expo-localization expo-linking @react-native-async-storage/async-storage

List the Expo modules in the command, as above: they are peer dependencies, and npx expo install adds the versions that match your app's Expo SDK. A plain npm install of the package alone installs the newest versions of missing peers, which may not match your Expo SDK; npx expo install --check reports any that don't.

No config plugin is needed. After installing, rebuild your development build if it doesn't already include these modules.

Platform Configuration

Universal Links (iOS) and App Links (Android) need your app to claim your link host. In Expo that is app config (app.json or app.config.*), not native project files:

{
  "expo": {
    "ios": {
      "associatedDomains": ["applinks:go.yourdomain.com"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": true,
          "data": [{ "scheme": "https", "host": "go.yourdomain.com" }],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

Add an entry for every host your links use. Your LinkForty server serves the Apple App Site Association and Digital Asset Links files on those hosts once your app's Apple Team ID / bundle ID and Android package / signing certificate SHA-256 are configured on it. Rebuild after changing this (npx expo prebuild if you manage native projects, or a new development / EAS build).

Expo Router: stop short links hitting "Unmatched Route"

Expo Router routes an incoming URL's path itself, so a short link like https://go.yourdomain.com/abc123 becomes the route /abc123 and shows "Unmatched Route". Add an app/+native-intent.tsx that leaves your link hosts to the SDK: your onDeepLink callback receives the link once it is resolved and navigates:

// app/+native-intent.tsx
const LINK_HOSTS = ['go.yourdomain.com']; // the hosts in app.json

export function redirectSystemPath({ path, initial }: { path: string; initial: boolean }) {
  try {
    const { hostname } = new URL(path);
    if (LINK_HOSTS.includes(hostname)) {
      // The SDK delivers this link to onDeepLink. On launch, start at the home
      // screen; while running, stay where the user is.
      return initial ? '/' : null;
    }
  } catch {
    // Not an absolute URL (a scheme link or plain path): let Expo Router handle it.
  }
  return path;
}

Keep this function free of anything that can throw: Expo Router calls it before your app's components run.

Quick Start

import LinkForty from '@linkforty/mobile-sdk-expo';

// Call once at app startup. A second call throws ALREADY_INITIALIZED,
// so guard code that fast refresh can re-run.
export async function setUpLinkForty() {
  if (LinkForty.isInitialized) return;

  const response = await LinkForty.initialize({
    baseUrl: 'https://api.linkforty.com', // the hosted service, or your own server's URL
    appToken: 'at_a1b2c3d4...',  // Required on the hosted service: scopes installs and link lookups to your workspace
    apiKey: 'your-api-key',      // Optional, only needed for createLink()
    debug: true,                 // Optional, enables verbose logging
    attributionWindowHours: 168, // Optional, default 7 days
  });

  // Check attribution
  if (response.attributed) {
    console.log('Install attributed!', response.deepLinkData);
  }

  // Listen for deferred deep links (new installs)
  LinkForty.onDeferredDeepLink((data) => {
    if (data) {
      console.log('Deferred deep link:', data.shortCode);
      // Navigate to content
    }
  });

  // Listen for direct deep links (existing users)
  LinkForty.onDeepLink((url, data) => {
    console.log('Deep link received:', url);
    if (data?.customParameters?.route) {
      // Navigate to route
    }
  });
}

Register both callbacks after initialize() resolves: they can only be registered once the SDK is initialized. Nothing is lost in between. The SDK handles the link that launched the app itself (from Linking.getInitialURL()), and a link handled while no onDeepLink callback is registered is held and delivered once to the first callback you register. The deferred deep link is kept too, so onDeferredDeepLink registered after initialize() still receives it.

API Reference

Initialization

await LinkForty.initialize(config);    // Returns InstallAttributionResponse
LinkForty.isInitialized;               // boolean getter

initialize() returns an InstallAttributionResponse on every launch. On the launch that reports the install it carries the server's answer; on later launches it is rebuilt from what was saved (attributed is true when install link data is saved, confidenceScore is 100 or 0, matchedFactors is empty). If the install report fails, it resolves as organic (attributed: false, empty installId) rather than rejecting, and onDeferredDeepLink receives null on that launch. The install stays unreported, so the next launch reports it again; when that report succeeds, that launch delivers the deferred deep link.

Upgrading from 1.9.0 or earlier: those versions marked a device as launched even when its first-launch install report failed, so the device never got an install ID and its events were rejected. On the first launch after upgrading, such a device (launched, but with no install ID saved) reports its install once, and that report only restores the install ID. It is not a new install: onDeferredDeepLink isn't called, initialize() resolves with attributed: false, and any link the server matches is neither saved as install data (getInstallData() stays null) nor credited for later events. These installs appear dated at the upgrade, and are usually organic, because the original click has fallen out of the attribution window. A device that has an install ID never reports again, and a device whose server answered without an install ID reports at most once.

Deep Linking

LinkForty.onDeferredDeepLink(callback); // Deferred (install attribution)
LinkForty.onDeepLink(callback);         // Direct (multiple callbacks supported)
LinkForty.handleDeepLink(url);          // Manual URL pass-through

The SDK listens for incoming links itself (expo-linking), including the link that launched the app, and passes each one to onDeepLink. Use handleDeepLink() only for URLs that reach your app another way: passing it the launch URL or a URL from your own Linking listener delivers that link to your callbacks twice.

A LinkForty link is resolved on the server for its customParameters, UTM parameters and metadata, falling back to a local parse if that fails; any other URL reaches onDeepLink with data set to null. A LinkForty link is an http(s) URL on your baseUrl host or, with appToken set, on one of your workspace's link hosts: its toapp.me label and verified custom domains. The SDK learns those hosts from the server at launch (GET /api/sdk/v1/link-domains) and saves them for the next one; on a server without that endpoint, such as @linkforty/core today, only the baseUrl host counts. With appToken set, a lookup answers only with your own workspace's links.

onDeferredDeepLink is called on the launch that reports the install: with the link's data, or null for an organic install. Later launches don't call it; getInstallData() returns the install's link data at any time. A launch whose install report failed also calls it, with null, and the next launch reports the install again (see Initialization).

On a matched install, the deferred deep link also carries the match's certainty when the server sends it: confidenceScore (0-100), matchedFactors, attributionMethod ('install_referrer' or 'fingerprint') and competingLinks. competingLinks counts the other links that had a click from the install's network that also reached the confidence threshold. Above 0, another device's tap may be what led to this install, so an app acting on a sensitive link (sign-in, gift) can treat the match as unverified, for example by asking the user to confirm. The InstallAttributionResponse returned by initialize() carries attributionMethod ('install_referrer', 'fingerprint' or 'none') and competingLinks too, on the launch that reports the install. Direct opens never carry these fields, and older servers omit them.

On Android, the SDK reads the Google Play install referrer (through expo-application) on the launch that reports the install, waiting at most 2 seconds, and sends it with the install. A LinkForty link puts its click id in the referrer of the Play listing it sends a visitor to, so a server on @linkforty/core 1.41.1 or later matches the install to that exact click: attributionMethod is 'install_referrer', confidenceScore 100 and competingLinks 0. Without a referrer (another store, a sideload, no answer from Play) the install is matched by fingerprint. iOS has no install referrer.

Event Tracking

await LinkForty.trackEvent(name, properties?);
await LinkForty.trackRevenue(amount, currency, properties?);
await LinkForty.flushEvents();          // Flush offline queue
await LinkForty.clearEventQueue();      // Clear without sending
LinkForty.queuedEventCount;            // Queue size getter

trackEvent() and trackRevenue() reject when the send fails (after the event has been queued; see Offline Resilience), so catch the error where you call them. Both need the install ID from the install report: on a launch whose install report failed, they reject with NOT_INITIALIZED and the event is not queued. The install is reported again on the next launch.

Every tracked event (including auto screen views) is automatically attributed to the deep link that drove the visit using a last-click model — so re-engagement campaigns are credited to the link the user tapped, not their original install link.

Automatic Screen Tracking

Auto-emit a screen_view event on each navigation — no manual per-screen calls — so your dashboard can show an attributed screen-flow funnel per campaign. Opt-in, via React Navigation (an optional peer dependency):

import LinkForty from '@linkforty/mobile-sdk-expo';
import { createNavigationContainerRef } from '@react-navigation/native';

export const navigationRef = createNavigationContainerRef();

await LinkForty.initialize({
  baseUrl: 'https://api.linkforty.com',
  appToken: 'at_a1b2c3d4...',
  autoTrackNavigation: true, // screen names only (privacy-safe default)
  navigationRef, // also pass this to <NavigationContainer ref={navigationRef}>
});

With Expo Router, there is no NavigationContainer to pass a ref to: use useNavigationContainerRef() from expo-router in your root layout and pass that ref. Both refs assign to navigationRef without a cast under strict TypeScript.

Route params are OFF by default (they can contain PII). To capture specific non-PII params, opt in with an explicit allow-list:

autoTrackNavigation: { captureParams: ['productId', 'category'] }

Apps that don't use React Navigation simply omit navigationRef — nothing changes and there's no required dependency. Rapid transitions are debounced (350 ms by default, debounceMs to change it) and same-screen re-renders deduped. Each screen_view carries screen, previousScreen (after the first) and params (allow-listed keys only).

Link Creation

Requires apiKey in config.

const result = await LinkForty.createLink({
  deepLinkParameters: { route: 'PRODUCT', id: '123' },
  title: 'Check out this product',
  utmParameters: { source: 'app', medium: 'share' },
});
console.log(result.url); // https://go.yourdomain.com/tmpl/abc123

Options: templateId, templateSlug, deepLinkParameters, title, description, customCode, utmParameters, externalUserId, webFallbackUrl. Without templateId the SDK calls POST /api/sdk/v1/links, which the hosted service serves and returns the link's full URL; with templateId it calls POST /api/links and builds the URL from baseUrl, templateSlug and the short code. A self-hosted @linkforty/core server doesn't support createLink(): it has no /api/sdk/v1/links, and its POST /api/links requires an originalUrl the SDK doesn't send.

User Identity

LinkForty.setExternalUserId('user-123'); // Attached to every createLink() unless overridden per call; null clears it
LinkForty.getExternalUserId();           // string | null
LinkForty.getSessionId();                // Current session id; rotates on cold start and each deep-link open

Attribution Data

await LinkForty.getInstallId();    // Cached install UUID
await LinkForty.getInstallData();  // Cached DeepLinkData
await LinkForty.isFirstLaunch();   // true until the install has been reported

Data Management

await LinkForty.clearData();  // Wipe all stored SDK data
LinkForty.reset();             // Return to uninitialized state

Named Exports

import { LinkFortySDK, LinkFortyError, LinkFortyErrorCode } from '@linkforty/mobile-sdk-expo';
import type {
  LinkFortyConfig,
  NavigationContainerRefLike,
  DeepLinkData,
  AttributionMethod,
  InstallAttributionResponse,
  UTMParameters,
  DeviceFingerprint,
  CreateLinkOptions,
  CreateLinkResult,
  EventRequest,
  AutoTrackNavigationOptions,
  DeferredDeepLinkCallback,
  DeepLinkCallback,
} from '@linkforty/mobile-sdk-expo';

Error Handling

The SDK is the package's default export (import LinkForty from '@linkforty/mobile-sdk-expo'); there is no named LinkForty export.

All SDK errors are instances of LinkFortyError with a .code property (and .statusCode for INVALID_RESPONSE):

| Code | When | |-------------------------|----------------------------------------------------------------| | NOT_INITIALIZED | Method called before initialize() | | ALREADY_INITIALIZED | initialize() called twice | | INVALID_CONFIGURATION | Bad config (missing or invalid baseUrl, HTTP on a host other than localhost, 127.0.0.1, 0.0.0.0 or 10.0.2.2, attribution window outside 1–2160 hours) | | NETWORK_ERROR | Network request failed after 3 attempts | | INVALID_RESPONSE | Server returned non-2xx response (4xx is not retried; 5xx is retried up to 3 attempts) | | DECODING_ERROR | Failed to parse server response | | INVALID_EVENT_DATA | Empty event name or negative revenue | | MISSING_API_KEY | createLink() called without API key | | INVALID_DEEP_LINK_URL | Defined for compatibility; not currently thrown |

Offline Resilience

An event that fails to send (offline, or any error after retries) is queued in AsyncStorage and survives app restarts. The queue holds 100 events; when it is full, new failed events are dropped. Queued events are sent, in order, when initialize() has reported the install, each time the app returns to the foreground, after the next event that sends successfully, and when you call flushEvents(). A send that fails leaves the rest of the queue for the next of these. The SDK doesn't watch connectivity.

Each event carries a clientEventId, kept with it in the queue, so every send of an event has the same ID. A server on @linkforty/core 1.36.0 or later, and the hosted service, store an event they have already received once, so an event whose send reached the server but whose response was lost isn't counted twice. Older servers ignore the field.

Configuration

| Field | Type | Required | Default | Description | |--------------------------|-----------|------------|-----------|-----------------------------------| | baseUrl | string | Yes | - | LinkForty server URL: https://api.linkforty.com on the hosted service, or your own server. HTTPS required except on localhost, 127.0.0.1, 0.0.0.0 and 10.0.2.2 | | appToken | string | Hosted: yes | - | Public workspace token (at_…), safe to ship in the app. Sent on every request: scopes install matching and link lookups to your workspace, attributes organic installs, and lets the SDK learn your workspace's link hosts | | apiKey | string | No | - | API key, only needed for createLink() | | debug | boolean | No | false | Enable verbose logging | | attributionWindowHours | number | No | 168 | Attribution window (1–2160 hours) | | autoTrackNavigation | boolean \| { captureParams?: string[]; debounceMs?: number } | No | false | Auto-emit screen_view from React Navigation (screen names only unless params are allow-listed) | | navigationRef | NavigationContainerRefLike | No | - | React Navigation ref; required when autoTrackNavigation is enabled |

Privacy

The SDK doesn't collect IDFA or GAID and never shows the App Tracking Transparency prompt. To match installs it sends: time zone, language, screen width and height, platform and OS version, app version, and a user agent built from your app's name and version; on Android, the Google Play install referrer. The server also sees the request's IP address. Link lookups send the time zone, language, screen size, platform and OS version. Events send the install ID, a random event ID, the event name and properties you pass, a timestamp, the session ID and the attributed link. Every request names the SDK and its version. Keep personal data out of event properties and captured route params.

Other SDKs

| Platform | Package | |----------|---------| | React Native | @linkforty/mobile-sdk-react-native | | iOS (Swift) | LinkFortySDK | | Android (Kotlin) | LinkFortySDK | | Flutter | linkforty |

Contributing

Contributions are welcome! Please open an issue or pull request.

License

MIT License - see LICENSE for details.

Support