Skip to content
IntegrateReact Native SDK

Integrate

React Native SDK

The reference for @flow/react-native, which catches the invite a bare React Native app was installed or opened with, and the native setup the app needs for it.

@flow/react-native is the Flow Affiliate SDK for React Native apps without Expo modules. It catches the invite your app was installed or opened with: the link, what your App Clip left, your deep-link SDK's params, the Play install referrer, or, if you turn it on, a probable match of the first launch to a recent click. What none of these catches is left to the code the person types into your code field, which normalizeCode reads. It only reacts to Flow Affiliate invites, never to your app's own referral links. It is plain JavaScript and holds no keys, and no money ever moves from the phone: your app sends what was caught to your backend, and your backend confirms the user to Flow Affiliate.

Its API is the same as @flow/expo's, and so is its behaviour: the two share their code. Only the native modules differ: react-native-device-info reads the Play install referrer and the phone's model and OS version. In an Expo app, use the Expo SDK.

npm install react-native-device-info @react-native-async-storage/async-storage
cd ios && pod install

react-native-device-info, react and react-native are peer dependencies: the SDK uses your app's own. Linking comes from react-native. It runs on iOS and Android, from React Native 0.73.

Set up the provider

Make one catcher for the app, outside any component, and put the provider at the root. The provider starts the catcher when the app mounts. It reads links on go.affiliate.yflow.io and its subdomains, your app's link host among them, without being told.

// affiliates.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createAffiliateCatcher } from '@flow/react-native';

export const affiliates = createAffiliateCatcher({
  storage: AsyncStorage,
  // only with a domain of your own
  linkHosts: ['go.yourapp.se'],
});
// App.tsx
import { AffiliateProvider } from '@flow/react-native';
import { affiliates } from './affiliates';

export default function App() {
  return (
    <AffiliateProvider catcher={affiliates}>
      <RootNavigator />
    </AffiliateProvider>
  );
}

Options

storageAffiliateStoragerequired

Where the install id and the first catch are kept: anything with getItem(key) and setItem(key, value), either async or not. Use AsyncStorage: it is emptied when the app is deleted, which is what once per install needs. Nothing in it is secret.

platform'ios' | 'android'

Defaults to Platform.OS. Anywhere else nothing is caught.

linkHostsstring[]

Your own link domains, read besides Flow Affiliate's go.affiliate.yflow.io and its subdomains (your app's link host among them). A link's /a/<code> is read only on these hosts; fg_click and fg_ref are read on any link. *. covers a host's subdomains.

handoffAffiliateStorage

The App Group storage your App Clip writes to with saveHandoff. See App Clip.

installMatchInstallMatchOptions

Turns on install matching: when nothing more certain came with the install, the first launch asks Flow Affiliate whether it matches a recent click. Off without it. See install matching.

InstallMatchOptions

appIdstringrequired
Your app's id in Flow Affiliate, a UUID. Not a secret: it is safe to ship in the app.
apiUrlstringrequired
Flow Affiliate's API, https://api.affiliate.yflow.io.
languagestring

The phone's language as a BCP 47 tag; only the language part is compared. Defaults to the JavaScript locale. With react-native-localize, pass getLocales()[0]?.languageTag.

device(platform) => InstallDevice

Where osVersion, model, screen and timezone come from. Defaults to react-native-device-info for getSystemVersion(), and getDeviceId() on iOS (iPhone15,2) or getModel() on Android (Pixel 8); React Native's screen in pixels, short side first (1179x2556); and the JavaScript time zone.

timeoutMsnumberdefault 5000
How long to wait for the answer. Without one, a later launch asks again.

The catcher checks the link the app was opened with, then on the first launch what your App Clip left (iOS) or the Play install referrer (Android), then your deep-link SDK's params, then the install match, and keeps the first that names a code or a click for the life of the install. useAffiliate() gives its state, kept current: ready, affiliate and installId, with ingestParams for Branch, AppsFlyer or Adjust params and reset for testing. The Expo SDK lists every field of the affiliate and the catcher; they are the same here.

Native setup

An installed app opens straight from a publisher's link, as a universal link on iOS and an app link on Android. First, under Tracking → Links, fill in your iOS app IDs (Team ID and bundle ID), your Android package name and its SHA-256 certificate fingerprints. Flow Affiliate then publishes /.well-known/apple-app-site-association and /.well-known/assetlinks.json on your app's link host, covering /a/*. Then declare the host in the app.

iOS

In Xcode, open your app target's Signing & Capabilities, add Associated Domains and list your app's link host, and your own domain if you have one:

applinks:yourapp.go.affiliate.yflow.io
applinks:go.yourapp.se

React Native's Linking only sees a universal link that the app delegate hands over. If your AppDelegate doesn't do it yet, add these two methods, as in React Native's linking guide:

// AppDelegate.swift, which imports React
// (add `override` to both when your AppDelegate subclasses RCTAppDelegate)
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
  RCTLinkingManager.application(app, open: url, options: options)
}

func application(_ application: UIApplication, continue userActivity: NSUserActivity,
                 restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
  RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler)
}

Android

In android/app/src/main/AndroidManifest.xml, add an intent filter for /a/ on your link host to the main activity. autoVerify makes Android check assetlinks.json and open the app without asking. Keep the activity's android:launchMode="singleTask", as React Native's template has it, so a link reaches the running app.

<activity android:name=".MainActivity" android:launchMode="singleTask" ...>
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="yourapp.go.affiliate.yflow.io" android:pathPrefix="/a/" />
  </intent-filter>
</activity>

Add a <data> line for your own domain if you have one. adb shell pm get-app-links <your package> shows whether Android verified the host.

The Play install referrer needs nothing more: react-native-device-info brings Google's Install Referrer library. On the first launch the SDK waits up to 3 seconds for Google Play's answer, so on a build that didn't come from Google Play, ready takes that long.

Send it to your backend

When something is caught, send its code and clickId with the sign-up. Your backend confirms the new user to Flow Affiliate with user.registered, server to server and signed with your app key; nothing from the phone moves money.

import { useAffiliate } from '@flow/react-native';

export function SignUpScreen() {
  const { ready, affiliate } = useAffiliate();

  async function signUp(form: SignUpForm) {
    await api.post('/signup', {
      ...form,
      affiliateCode: affiliate?.code ?? form.inviteCode, // or what the person typed
      clickId: affiliate?.clickId,
    });
  }
  // when ready and nothing was caught, show the code field
}
// your backend, with @flow/growth-sdk
await flow.track({ type: 'user.registered', eventId: `registered:${user.id}`, appUserId: user.id, affiliateCode: body.affiliateCode, clickId: body.clickId });

See user.registered. To show "Invited by Anna" before sign-up, your backend can first forward affiliate as it is to POST /v1/attributions.

App Clip

An App Clip opens from a link without an install and leaves the invite for the full app, through an App Group both targets share. React Native has no App Group storage of its own, so the SDK takes any AffiliateStorage over the group: getItem(key) and setItem(key, value). For example, with react-native-shared-group-preferences, which reads and writes the group's UserDefaults:

// group-storage.ts
import SharedGroupPreferences from 'react-native-shared-group-preferences';
import type { AffiliateStorage } from '@flow/react-native';

const APP_GROUP = 'group.se.yourapp.app';

export const groupStorage: AffiliateStorage = {
  getItem: async (key) => {
    // it rejects when nothing was saved, and may hand back JSON already parsed
    const value: unknown = await SharedGroupPreferences.getItem(key, APP_GROUP).catch(() => null);
    return value == null ? null : typeof value === 'string' ? value : JSON.stringify(value);
  },
  setItem: (key, value) => SharedGroupPreferences.setItem(key, value, APP_GROUP),
};
// in the App Clip: catch the link it was opened with, then leave it for the full app
import { createAffiliateCatcher, saveHandoff } from '@flow/react-native';

const clip = createAffiliateCatcher({ storage: AsyncStorage });
void clip.start().then(({ affiliate }) => saveHandoff(affiliate, groupStorage));
// in the full app: read what the clip left, on the first launch
export const affiliates = createAffiliateCatcher({
  storage: AsyncStorage,
  handoff: Platform.OS === 'ios' ? groupStorage : undefined,
});

Any other App Group storage works the same way, such as an MMKV instance in the group. What the clip left comes out with source: 'app_clip', as sure as a link open, for 30 days. A native App Clip in Swift can leave the same thing itself: a JSON string under flowaffiliate.handoff in the group's UserDefaults, with code (fg_ref), clickId (fg_click), url and savedAt in milliseconds.

Typed codes

For your code field, the fallback for whatever the automatic catch missed. normalizeCode reads the code itself, also typed in groups, or the code in a Flow Affiliate link, or a message with one, pasted into the field. Codes are 3 to 24 letters and digits and match whatever their case. Null when there is none.

import { normalizeCode } from '@flow/react-native';

normalizeCode(' K7QX A2MP ');                                       // 'K7QXA2MP'
normalizeCode('Join me: https://yourapp.go.affiliate.yflow.io/a/k7qxa2mp'); // 'k7qxa2mp'
normalizeCode('hello!');                                            // null

With a domain of your own, pass it too: normalizeCode(text, { linkHosts }).

Send it as affiliateCode. Flow Affiliate only counts its own codes, so a field you already have for your own referral codes keeps working. See the code field.

What it never does

  • Touch the clipboard. It never reads or checks it.
  • Match without your say. The install match runs only with installMatch. It sends the OS version, model, screen size, language, time zone and install id; Flow Affiliate sees the IP address the request comes from and keeps only a keyed hash of it.
  • Hold a key, or move money. Your backend confirms users with its own signed key.