Skip to content
IntegrateExpo SDK

Integrate

Expo SDK

The reference for @flow/expo, which catches the invite an Expo app was installed or opened with and hands it over in the shape POST /v1/attributions takes.

@flow/expo 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. The only request it makes is the install match, when you turn it on. It gives your app a referral, which your app sends to your backend, and your backend forwards to POST /v1/attributions. Links into your app explains each way in and what to set up for it.

npx expo install expo-application expo-device @react-native-async-storage/async-storage

expo-application, expo-device, react and react-native are peer dependencies: the SDK uses your app's own. Linking comes from react-native, so expo-linking is not needed. It runs on iOS and Android, from Expo SDK 50.

createAffiliateCatcher

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

export const affiliates = createAffiliateCatcher({ storage: AsyncStorage });

Make one for the app, outside any component.

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. SecureStore works too, but the iOS keychain behind it outlives a reinstall.

platform'ios' | 'android'

Defaults to Platform.OS. Anywhere else, such as the web, nothing is caught.

linkHostsstring[]default ['go.affiliate.yflow.io', '*.go.affiliate.yflow.io']

The hosts that serve your Flow Affiliate links. A link's /a/<code> is read only on these; fg_click and fg_ref are read on any link. *. covers a host's subdomains, so the default already reads your app's link host. With a domain of your own, list it and your app's link host, as Tracking → Links → Advanced → Tracking host shows it.

handoffAffiliateStorage

The App Group storage your App Clip writes to with saveHandoff. On the first launch, what the clip left is read after the link the app was opened with. 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.flowgroup.se.
languagestring

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

device(platform) => InstallDevice

Where osVersion, model, screen and timezone come from. Defaults to expo-device for osVersion, and modelId on iOS (iPhone15,2) or modelName on Android (Pixel 8); React Native's screen in pixels, short side first (1179x2556); and the JavaScript time zone (Europe/Stockholm).

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

It checks the channels in this order and keeps the first that names a code or a click:

  1. The link the app was opened with, on every launch until something is caught, and every link that opens the app after that: universal links, app links and custom schemes. source is link.
  2. What your App Clip left, on iOS's first launch, when you pass handoff. source is app_clip.
  3. The Play install referrer, on Android's first launch. source is play_referrer.
  4. Your deep-link SDK's params, when you hand them over with ingestParams before the install match answers. source is branch for Branch, other for anything else.
  5. The install match, with installMatch, once per install: on the first launch, or a later one within 72 hours if Flow Affiliate wasn't reached. On Android only when Google Play gave no referrer. source is install_match and confidence is probable.

Only Flow Affiliate invites count: fg_click and fg_ref, or /a/<code> on one of linkHosts. Your app's own referral links and keys, such as code, are never read. A code typed into a field is not caught automatically: your backend sends it as code. See the code field.

The first catch is kept for the life of the install. Later catches are still answered, so you can send them, but affiliate doesn't change.

The affiliate

Affiliate is the body of POST /v1/attributions, typed from the API's own OpenAPI document. Send it to your backend as it is; your backend forwards it, adding appUserId only for a user who already has an account.

{
  "url": "https://go.affiliate.yflow.io/a/k7qxa2mp?fg_click=1d4f9c5e-7a8b-4c2d-9e1f-0a1b2c3d4e5f",
  "code": "k7qxa2mp",
  "clickId": "1d4f9c5e-7a8b-4c2d-9e1f-0a1b2c3d4e5f",
  "source": "link",
  "confidence": "deterministic",
  "platform": "ios",
  "installId": "5b1d0c9e-2f4a-4b8c-9d7e-6f5a4b3c2d1e"
}

Affiliate

urlstring
The link the app was opened with.
paramsobject
Your deep-link SDK's referring params, exactly as you handed them over.
codestring
The code, read on the device from whatever was caught. Use it to show who invited the person.
clickIdstring
The click id, read on the device the same way, or the one the install match answered.
sourcestringrequired
link, app_clip, branch, play_referrer, install_match or other.
confidencestring
deterministic for a link, the App Clip or the Play referrer; probable for an install match. For deep-link params, Branch's +match_guaranteed as deterministic or probable; otherwise left out.
platformstringrequired
ios or android.
installIdstringrequired
A random id made at the install's first launch. Asking again with it answers with the same attribution key.
publisherNamestring
An install match only: the publisher's display name, for "Invited by Anna". For display; the ask ignores it.

The catcher

ReferralCatcher

start()Promise<ReferralState>

Checks what this launch brought, once, then listens for links. Calling it again answers with the same check. AffiliateProvider calls it for you.

stop()void
Stops listening for links. start() listens again.
getState()ReferralState
The state now.
subscribe(listener)() => void
Calls listener on every change; returns the unsubscribe.
ingestParams(params)Promise<Affiliate | null>

Your deep-link SDK's params: Branch's subscribe params, AppsFlyer's deep link data, or Adjust's deferred deep link as { deeplink }. Answers null for params that name no code or click, such as an organic open. Waits for the link check, so a link the app was opened with still comes first, but not for the install match.

reset()Promise<ReferralState>
Forgets the install id and the catch, as a reinstall would, and checks again. For testing.

ReferralState

readyboolean
This launch's check is done, the install match included.
affiliateAffiliate | null
What this install was first caught with, kept until the app is deleted.
installIdstring | null
This install's id; null until ready.

React

AffiliateProvider takes the catcher and starts it when it mounts. Inside it, useAffiliate() returns the state, kept current, with ingestParams and reset.

import { AffiliateProvider, useAffiliate } from '@flow/expo';

<AffiliateProvider catcher={affiliates}>
  <App />
</AffiliateProvider>;

function SignUp() {
  const { ready, affiliate } = useAffiliate();
  // nothing caught, as after an install from the App Store: show the code field
  // …
}

saveHandoff and readHandoff

For an App Clip. In the clip, saveHandoff(affiliate, storage) leaves the invite it was opened with in storage, which must be backed by the App Group the clip shares with the full app. It answers false when there was nothing to leave or it could not be written. In the full app, pass the same storage as the catcher's handoff option; readHandoff(storage) is what the catcher calls, answering the invite with source: 'app_clip', or null when the clip left nothing or left it more than 30 days ago.

// in the App Clip
const clip = createAffiliateCatcher({ storage: AsyncStorage });
void clip.start().then(({ affiliate }) => saveHandoff(affiliate, groupStorage));

normalizeCode

For your code field, the fallback for whatever the automatic catch missed: on iPhone nothing passes through the App Store, and an install match can miss. It is not part of the automatic catch: what it reads is yours to send, as code.

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

The code in what someone typed or pasted: the code itself, also typed in groups, or a Flow Affiliate link, or a message with one, that the code is read from. A link's /a/<code> counts only on go.affiliate.yflow.io and its subdomains, or on the hosts you pass as normalizeCode(text, { linkHosts }). Null when there is no code of 3 to 24 letters and digits. The case is kept as typed: Flow Affiliate matches codes whatever their case.

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. See install matching.
  • Hold a key, or ask about attribution. Your backend asks, with its own signed key. The install match is a question about a click, not an attribution, and needs no key.