Skip to content
IntegrateiOS SDK (Swift)

Integrate

iOS SDK (Swift)

The reference for FlowAffiliate, the native Swift SDK that catches the invite an iOS app or App Clip was opened or installed with and hands it over in the shape POST /v1/attributions takes.

FlowAffiliate catches the invite your iOS app was installed or opened with, once per install. It can come from the link, from what your App Clip left, from your deep-link SDK's params, or, if you turn it on, from a probable match of the first launch to a recent click. What none of these catch is left to the code the person types into your code field, which normalizeCode reads. The SDK 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. Your app sends it to your backend, which confirms the new user to Flow with POST /v1/attributions. Links into your app explains each way in and what to set up for it.

It needs iOS 14 or later and Xcode 16 or later. It depends on nothing but Foundation and Combine, so it also fits in an App Clip. It behaves like the Expo SDK, so an app that mixes the two works: for example an Expo app with a native App Clip.

Install

Swift Package Manager. In Xcode, choose File → Add Package Dependencies…, enter the URL below and add the FlowAffiliate library to your app target, and to your App Clip target if you have one. In a Package.swift:

.package(url: "https://github.com/Ebuno-AB/flow-affiliate-ios", from: "0.1.0")

CocoaPods. In your Podfile, for the app target and the App Clip target:

pod 'FlowAffiliate', '~> 0.1'

Configure and start

Configure once at launch, then call start(). In SwiftUI:

import FlowAffiliate
import SwiftUI

@main
struct YourApp: App {
    init() {
        FlowAffiliate.configure(
            appId: "6f1c2b3a-4d5e-4f60-8a7b-9c0d1e2f3a4b", // your app's id in Flow: turns on install matching
            linkHosts: ["go.yourapp.com"],                // your own link domain, if you have one
            appGroup: "group.com.yourapp"                 // only with an App Clip
        )
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { FlowAffiliate.handle(url: $0) }
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { FlowAffiliate.handle(userActivity: $0) }
                .task { await FlowAffiliate.start() }
        }
    }
}

configure

appIdString?

Your app's id in Flow Affiliate, a UUID. It is not a secret, so it is safe to ship in the app. It turns on install matching. Without it, the SDK makes no requests.

apiURLURLdefault https://api.affiliate.yflow.io
Flow Affiliate's API.
linkHosts[String]

Your own link domains, added to Flow Affiliate's: go.affiliate.yflow.io and any subdomain of it, such as yourapp.go.affiliate.yflow.io, are always read. A link's /a/<code> is read only on these hosts; fg_click and fg_ref are read on any link. An entry such as *.yourapp.com covers every subdomain of that domain.

appGroupString?

The App Group your App Clip shares with the app. On the first launch, what the clip left there is read. See App Clip.

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, handed over with handle(url:) or handle(userActivity:), on any launch until something is caught: universal links and custom schemes. A link handed over before start() comes first. source is link.
  2. What your App Clip left, on the first launch, when you pass appGroup. source is app_clip.
  3. Your deep-link SDK's params, when you hand them over with ingest(params:). source is branch for Branch, other for anything else.
  4. The install match, when you pass appId, once per install: on the first launch, or on a later one within 72 hours if Flow Affiliate wasn't reached. source is install_match and confidence is probable.

Only Flow Affiliate invites count: fg_click and fg_ref, 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, with a random installId, in UserDefaults, which is emptied when the app is deleted. Later catches are still answered, so you can send them, but affiliate doesn't change.

Call FlowAffiliate from the main thread, as SwiftUI and the app and scene delegates do. FlowAffiliateClip can be called from any thread.

FlowAffiliate

start()async → AffiliateState

Checks what this launch brought, once: the stored catch, the App Clip handoff, then the install match. Calling it again answers with the same check.

handle(url:)Affiliate?
A universal link or custom-scheme URL. Answers with the invite it carries, or nil for any other link, which you then route as usual.
handle(userActivity:)Affiliate?
A universal link as an NSUserActivity (NSUserActivityTypeBrowsingWeb).
ingest(params:)Affiliate?
Your deep-link SDK's params, as they were handed over. Nil for params that name no invite, such as an organic open.
affiliateAffiliate?
What this install was first caught with; nil until something is caught.
stateAffiliateState
ready (this launch's check is done, the install match included), affiliate and installId.
publisherAnyPublisher<AffiliateState, Never>
The state now, then every change, on the main thread.
normalizeCode(_:)String?
The code in what someone typed. See typed codes.
invite(code:)async → AffiliateInvite?
The publisher's greeting and the invitee's bonus, for "Invited by Anna". Nil after 2 seconds.
reset()async → AffiliateState
Forgets the install id and the catch, as a reinstall would, and checks again. For testing.

An installed app opens straight from a publisher's link. In your program's setup, step 3 Your app in the stores, under Advanced, fill in your iOS app IDs (Team ID and bundle ID). Flow Affiliate then publishes /.well-known/apple-app-site-association on your app's link host, covering /a/*. Then add the Associated Domains capability to your app target with your link host, as Tracking host shows it, and your own domain if you have one:

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

SwiftUI. Hand every URL to the SDK, as in the example above. Universal links arrive at .onOpenURL, or at .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) when you have one. A link handed over twice still counts once. Route links the SDK answers nil for as usual.

UIKit with scenes. A cold start brings the link in the connection options; a running app gets it through the two callbacks:

import FlowAffiliate
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        connectionOptions.urlContexts.forEach { FlowAffiliate.handle(url: $0.url) }
        connectionOptions.userActivities.forEach { FlowAffiliate.handle(userActivity: $0) }
        Task { await FlowAffiliate.start() }
    }

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        URLContexts.forEach { FlowAffiliate.handle(url: $0.url) }
    }

    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        FlowAffiliate.handle(userActivity: userActivity)
    }
}

Call FlowAffiliate.configure(…) in application(_:didFinishLaunchingWithOptions:). Without scenes, use application(_:open:options:) and application(_:continue:restorationHandler:) the same way.

The affiliate

Affiliate is Codable, and encodes to the body of POST /v1/attributions. 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://yourapp.go.affiliate.yflow.io/a/k7qxa2mp",
  "code": "k7qxa2mp",
  "source": "link",
  "confidence": "deterministic",
  "platform": "ios",
  "installId": "5b1d0c9e-2f4a-4b8c-9d7e-6f5a4b3c2d1e"
}

Affiliate

codeString?
The publisher's code. Use it to show who invited the person.
clickIdString?
The click id, from the link or the install match.
urlString?
The link the app was opened with, or the App Clip's.
params[String: String]?
Your deep-link SDK's params, values as text.
sourceAffiliateSourcerequired
link, app_clip, branch, install_match or other.
confidenceAffiliateConfidence?
deterministic for a link or the App Clip, probable for an install match. For deep-link params, Branch's +match_guaranteed; otherwise nil.
platformStringrequired
Always ios.
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 only; the ask ignores it.

Send it to your backend

Send the affiliate as soon as you have it, before sign-up if you like. Asking binds nothing. publisher gives you the stored catch at launch and a link that opens the app later:

.onReceive(FlowAffiliate.publisher.compactMap(\.affiliate).removeDuplicates()) { affiliate in
    Task {
        var request = URLRequest(url: URL(string: "https://api.yourapp.com/affiliate/attribution")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "content-type")
        request.httpBody = try? JSONEncoder().encode(affiliate)
        _ = try? await URLSession.shared.data(for: request)
    }
}

Your backend asks POST /v1/attributions with its own key, keeps the attribution key with the sign-up, and confirms the new user with user.registered. The phone never talks to Flow about attribution.

For a SwiftUI view, AffiliateModel is an ObservableObject with the same state, kept current:

struct SignUpView: View {
    @StateObject private var invite = AffiliateModel()

    var body: some View {
        if let code = invite.affiliate?.code {
            Text("Invited with code \(code)")
        } else if invite.ready {
            CodeField() // nothing was caught: let them type the code
        }
    }
}

If you already use Branch, AppsFlyer or Adjust, hand their params over as they are. Flow Affiliate reads fg_ref and fg_click from their keys, and from a Flow Affiliate link among their values, such as Branch's ~referring_link. Your own keys are ignored.

// Branch
Branch.getInstance().initSession(launchOptions: launchOptions) { params, _ in
    FlowAffiliate.ingest(params: params)
}

// AppsFlyer, in your DeepLinkDelegate
func didResolveDeepLink(_ result: DeepLinkResult) {
    FlowAffiliate.ingest(params: result.deepLink?.clickEvent)
}

// Adjust, the deferred deep link
func adjustDeferredDeeplinkReceived(_ deeplink: URL?) -> Bool {
    if let deeplink { FlowAffiliate.ingest(params: ["deeplink": deeplink]) }
    return true
}

App Clip

An App Clip catches the click on an iPhone that doesn't have your app yet. It is optional: see App Clip for when to use one. A phone without your app goes to Apple's App Clip link, https://appclip.apple.com/id?p=<App Clip bundle ID>, with fg_click and fg_ref added. The clip leaves them in the App Group it shares with your app, and your app's first launch reads them there.

  1. An App Clip target with a bundle ID such as com.yourapp.Clip, with FlowAffiliate added to it.
  2. An App Group, such as group.com.yourapp, in both targets' App Groups capability.
  3. In the clip, save the invocation URL and, if you like, show who invited them:
import FlowAffiliate
import SwiftUI

@main
struct YourAppClip: App {
    @State private var invite: AffiliateInvite?

    var body: some Scene {
        WindowGroup {
            ClipView(invite: invite) // the greeting, the bonus and an "Install the app" button
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    guard let url = activity.webpageURL else { return }
                    FlowAffiliateClip.saveHandoff(url: url, appGroup: "group.com.yourapp")
                    Task { invite = await FlowAffiliateClip.invite(url: url) }
                }
        }
    }
}
  1. In the app, pass the same group: FlowAffiliate.configure(appGroup: "group.com.yourapp"). What the clip left comes out on the first launch with source: app_clip, as sure as a link open. It is kept for 30 days.

saveHandoff(url:appGroup:) answers false when the URL carries no invite or the group can't be written. invite(url:) fetches GET /v1/share/landing/:code with a 2-second limit: greeting, bonus (such as "5 USD"), appName and appColor, or nil. It writes the same file as the Expo SDK's saveHandoff, flowaffiliate.handoff in the group's container, so a native clip and an Expo app, or the other way round, work together.

To test, set _XCAppClipURL in the clip scheme's environment to https://appclip.apple.com/id?p=com.yourapp.Clip&fg_click=<a click id>&fg_ref=<a code>. Run the clip, then install and open the app on the same simulator: FlowAffiliate.affiliate should have source: app_clip. Then, in your program's setup, step 3 Your app in the stores, under Advanced, enter the App Clip bundle ID and choose App Clip as the Route for iPhones. Do this only once the clip is live in the App Store.

Typed codes

Every app needs a code field: on iPhone nothing passes through the App Store, and an install match can miss. normalizeCode reads what someone typed or pasted. That can be the code itself, also typed in groups, or a Flow Affiliate link, or a message with one. It answers nil when there is no code of 3 to 24 letters and digits. It keeps the case as typed, since Flow Affiliate matches codes whatever their case. It is not part of the automatic catch: what it reads is yours to send, as code.

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!")                                       // nil

A link's /a/<code> counts only on Flow Affiliate's hosts and the linkHosts you configured.

What it never does

  • Touch the clipboard. It never reads or checks it.
  • Match without your say. The install match runs only with appId. It sends the OS version, the model identifier (iPhone15,2), the screen size in pixels, the language, the time zone and the 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.