Integrate
Android SDK
The reference for the Android SDK, which catches the invite a native Android app was installed or opened with and hands it over in the shape POST /v1/attributions takes. Kotlin, and usable from Java.
The Android SDK catches the invite your app was installed or opened with: an App Link to /a/<code>, the Play install referrer, your deep-link SDK's params, or, if nothing else came, a probable match of the first launch to a recent click. It catches once per install and keeps what it caught. It only reacts to Flow Affiliate invites, never to your app's own referral links. It holds no keys, and no money ever comes from the phone. It gives your app an affiliate, which your app sends to your backend, and your backend forwards to POST /v1/attributions. Links into your app explains each way in.
It is written in Kotlin, works from Java, and runs from Android 5.0 (minSdk 21).
Install
// app/build.gradle.kts
dependencies {
implementation("io.yflow:affiliate-android:0.1.0")
}It brings Google's Play Install Referrer library and kotlinx-coroutines, and adds the INTERNET permission to your manifest. Nothing else.
Set up
Set it up once in your Application, hand it the intent your Activity is opened with, and start it at launch. Hand the intent over before you call start(), so a link counts ahead of the Play referrer.
class App : Application() {
override fun onCreate() {
super.onCreate()
FlowAffiliate.init(this, FlowAffiliateConfig(appId = "6f1c2b3a-4d5e-4f60-8a7b-9c0d1e2f3a4b"))
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
FlowAffiliate.handle(intent)
lifecycleScope.launch {
val affiliate = FlowAffiliate.start() // null when nothing was caught
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
FlowAffiliate.handle(intent)
}
}public class App extends Application {
@Override public void onCreate() {
super.onCreate();
FlowAffiliate.init(this, new FlowAffiliateConfig("6f1c2b3a-4d5e-4f60-8a7b-9c0d1e2f3a4b"));
}
}
public class MainActivity extends AppCompatActivity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FlowAffiliate.handle(getIntent());
// called on the main thread when the check is done; null when nothing was caught
FlowAffiliate.start(affiliate -> { });
}
@Override protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
FlowAffiliate.handle(intent);
}
}FlowAffiliateConfig
appIdStringrequiredYour app's id in Flow Affiliate, a UUID, shown as App ID in the program's Install the SDK step. Not a secret: it is safe to ship in the app.
apiUrlStringlinkHostsList<String>Your own link domain, if you have one, such as go.yourapp.com; *.yourapp.com covers its subdomains. They are added to Flow Affiliate's, FlowAffiliateConfig.DEFAULT_LINK_HOSTS: go.affiliate.yflow.io and every subdomain of it, your app's link host yourapp.go.affiliate.yflow.io included. A link's /a/<code> is read only on these; fg_ref and fg_click on any link.
installMatchBooleanWith nothing else caught, the first launch asks Flow Affiliate whether it matches a recent click. false turns it off. See install matching.
timeoutMsLongIt checks the channels in this order and keeps the first that names a code or a click:
- The link the app was opened with, handed over with
handle(intent), on every launch until something is caught.sourceislink. - The Play install referrer, on the first launch, unless a link came first.
sourceisplay_referrer. - Your deep-link SDK's params, handed over with
ingest.sourceisbranchfor Branch,otherfor anything else. - The install match, once per install: on the first launch, or a later one within 72 hours if Flow Affiliate wasn't reached. Only when nothing above caught anything.
sourceisinstall_matchandconfidenceisprobable.
Only Flow Affiliate invites count: fg_click and fg_ref on any link, or /a/<code> on a link host. Your app's own referral links and keys, such as code, are never read. The first catch is kept for the life of the install; later ones are answered (handle returns true) but affiliate doesn't change.
The install id and the catch are kept in SharedPreferences, which Android deletes with the app. When Android's backup restores them onto a new install, the SDK sees that the install is not the one that saved them and starts over, so a reinstall is a new install.
App Links
An installed app opens straight from a publisher's link. Your app proves it owns the link host, and handle(intent) reads the link.
- Under Tracking → Links, fill in your Android package name and its SHA-256 certificate fingerprints. Flow Affiliate then serves
/.well-known/assetlinks.jsonon your app's link host, built from exactly those fingerprints and covering/a/*. - Enter the fingerprint of your Play App Signing key: Google Play re-signs your app with it, so it is the certificate Android checks on every phone that installed from Play. Play Console shows it under App integrity, as the app signing key certificate. Add your upload key and your debug key too, to test builds you install yourself.
- Declare the link host in your manifest, as Tracking → Links → Advanced → Tracking host shows it, with
autoVerifyand the/a/path. Add your own domain as anotherdatahost if you have one.
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop">
<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" />
<data android:host="yourapp.go.affiliate.yflow.io" />
<data android:pathPrefix="/a/" />
</intent-filter>
</activity>Check it on a phone:
adb shell pm verify-app-links --re-verify com.yourapp
adb shell pm get-app-links com.yourapp # the host should say "verified"
adb shell am start -a android.intent.action.VIEW -d "https://yourapp.go.affiliate.yflow.io/a/k7qxa2mp"An Android phone without your app goes to Google Play instead, with the click in the install referrer. There is nothing to set up in the app for that: paste your Google Play link under Tracking → Links. Only installs from Google Play carry it.
The affiliate
Affiliate is the body of POST /v1/attributions. toJson() gives it as JSON: send it to your backend as it is, and your backend forwards it, adding appUserId only for a user who already has an account.
{
"url": "https://yourapp.go.affiliate.yflow.io/a/k7qxa2mp",
"code": "k7qxa2mp",
"source": "link",
"confidence": "deterministic",
"platform": "android",
"installId": "5b1d0c9e-2f4a-4b8c-9d7e-6f5a4b3c2d1e"
}Affiliate
codeString?clickIdString?sourceAffiliateSourcerequiredLINK, PLAY_REFERRER, BRANCH, INSTALL_MATCH or OTHER; .value is the wire name, link and so on.confidenceAffiliateConfidence?DETERMINISTIC for a link or the Play referrer, PROBABLE for an install match. For deep-link params, Branch's +match_guaranteed; otherwise null.installIdStringrequiredurlString?paramsMap<String, Any?>?publisherNameString?platformStringandroid.Send it to your backend
Asking binds nothing, so send it as soon as you have it, before sign-up if you like. affiliate holds the kept catch from the start of every launch; asking again with the same installId answers with the same key, so sending it again is safe.
lifecycleScope.launch {
FlowAffiliate.affiliate.filterNotNull().collect { affiliate ->
backend.post("/affiliate/attribution", affiliate.toJson())
}
}FlowAffiliate.start(affiliate -> {
if (affiliate != null) backend.post("/affiliate/attribution", affiliate.toJson());
else showCodeField(); // nothing caught: ask for the code at sign-up
});
// a link that opens the running app later
FlowAffiliate.addListener(affiliate -> {
if (affiliate != null) backend.post("/affiliate/attribution", affiliate.toJson());
});Your backend asks Flow Affiliate with its own signed key, and keeps the attribution key until it knows whether the user is new. Then it confirms with user.registered. See Attribution for apps.
// your backend, with the Node SDK
const ask = await fg.createAttribution(req.body);
if (ask.ok && ask.data.attributable) await saveForInstall(req.body.installId, ask.data.attributionKey);Deep-link SDKs
If links reach your app through Branch, AppsFlyer or Adjust, hand their params over with ingest. Only fg_ref and fg_click, and Flow Affiliate links among the values, are read. It returns true when they carry an invite.
// Branch: the JSONObject from onInitFinished
FlowAffiliate.ingest(referringParams)
// AppsFlyer: the deep link's click event, or the conversion data map
FlowAffiliate.ingest(deepLinkResult.deepLink?.clickEvent)
// Adjust: the deferred deep link
FlowAffiliate.ingest(mapOf("deeplink" to deeplink.toString()))Typed codes
Every app needs a field where the person can enter the publisher's code: an install match can miss, and a phone that didn't install from Google Play carries no referrer. Show it at sign-up when nothing was caught, and fill it in when something was. normalizeCode reads what someone typed, also in groups, or the code in a Flow Affiliate link or a message with one. Codes are 3 to 24 letters and digits, and match whatever their case.
FlowAffiliate.normalizeCode(" K7QX A2MP ") // "K7QXA2MP"
FlowAffiliate.normalizeCode("https://go.affiliate.yflow.io/a/k7qxa2mp") // "k7qxa2mp"
FlowAffiliate.normalizeCode("Join me: https://yourapp.go.affiliate.yflow.io/a/k7qxa2mp") // "k7qxa2mp"
FlowAffiliate.normalizeCode("hello!") // nullIt is not part of the automatic catch: your backend asks with it as code. A code that isn't Flow Affiliate's answers unknown_code, so a field you already have for your own referral codes keeps working. See the code field.
The invite greeting
Optional. invite(code) answers what Flow Affiliate's invite page shows for a code (GET /v1/share/landing/:code): the greeting the publisher chose, the welcome bonus if the link carries one, and the app's name, logo, colour and tagline. Null for a code that isn't live, or when Flow Affiliate doesn't answer within 2 seconds.
val invite = FlowAffiliate.invite(affiliate.code ?: return)
// invite?.greeting: "Anna invites you to Yourapp", invite?.bonus: "5 USD"From Java, FlowAffiliate.invite(code, invite -> { … }) calls back on the main thread.
FlowAffiliate
FlowAffiliate
init(context, config)UnitApplication.onCreate. Only the first call counts.start()suspend Affiliate?Checks what this launch brought, once, and answers with the affiliate, or null when nothing was caught. Calling it again answers with the same check. From Java, start(listener) calls back on the main thread.
handle(intent)BooleanonCreate and onNewIntent. Also takes a Uri or a String. True when it carries an invite.ingest(params)BooleanMap or Branch's JSONObject. True when they carry an invite.affiliateStateFlow<Affiliate?>getAffiliate().getValue().addListener(listener)Unitlistener on the main thread whenever affiliate changes; removeListener stops it.installIdString?start() has read it.normalizeCode(input)String?/a/<code> on your linkHosts too.invite(code)suspend AffiliateInvite?reset()UnitWhat it never does
- Touch the clipboard. It never reads or checks it.
- Hold a key, or ask about attribution. Your backend asks, with its own signed key. No money comes from the phone.
- Match without saying so. The install match sends the OS version, model, screen size, language, time zone and install id, and nothing else; Flow Affiliate sees the IP address the request comes from and keeps only a keyed hash of it.
installMatch = falseturns it off.