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-storageexpo-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
storageAffiliateStoragerequiredWhere 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[]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.
handoffAffiliateStorageThe 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.
installMatchInstallMatchOptionsTurns 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
appIdstringrequiredapiUrlstringrequiredhttps://api.flowgroup.se.languagestringThe 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) => InstallDeviceWhere 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).
timeoutMsnumberIt checks the channels in this order and keeps the first that names a code or a click:
- 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.
sourceislink. - What your App Clip left, on iOS's first launch, when you pass
handoff.sourceisapp_clip. - The Play install referrer, on Android's first launch.
sourceisplay_referrer. - Your deep-link SDK's params, when you hand them over with
ingestParamsbefore the install match answers.sourceisbranchfor Branch,otherfor anything else. - 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.sourceisinstall_matchandconfidenceisprobable.
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
urlstringparamsobjectcodestringclickIdstringsourcestringrequiredlink, app_clip, branch, play_referrer, install_match or other.confidencestringdeterministic 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.platformstringrequiredios or android.installIdstringrequiredpublisherNamestringThe 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()voidstart() listens again.getState()ReferralStatesubscribe(listener)() => voidlistener 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>ReferralState
readybooleanaffiliateAffiliate | nullinstallIdstring | nullReact
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!'); // nullThe 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.