Skip to content

The Frak iOS SDK brings referral tracking, rewards, and the sharing sheet to a native iOS app. It is a Swift package with zero third-party dependencies, split in two products so an app that only needs tracking never links a web view.

ItemValue
Minimum iOS15
Minimum Xcode16 (the package declares Swift 6 language mode)
DependenciesNone
LicenseApache-2.0

In Xcode: File → Add Package Dependencies, then enter https://github.com/frak-id/frak-ios-sdk.

Or in a Package.swift:

Package.swift
dependencies: [
.package(url: "https://github.com/frak-id/frak-ios-sdk.git", exact: "1.0.0-beta.1")
],
targets: [
.target(name: "YourApp", dependencies: [
.product(name: "FrakSDK", package: "frak-ios-sdk"),
// Only if you show the sharing sheet
.product(name: "FrakSDKUI", package: "frak-ios-sdk"),
])
]
ProductContents
FrakSDKIdentity, config, rewards, interaction tracking, sharing links, app links. No UI, no web view.
FrakSDKUIAdds the sharing sheet, a WKWebView inside a SwiftUI sheet. Depends on FrakSDK.

The dependency only runs one way, so taking FrakSDK alone links no web view.

FrakSDKVersion.current returns the version the SDK reports on every request, which is useful in a bug report. It is the one public type absent from the API reference: Swift’s symbol graph extractor drops any declaration whose name starts with the module name followed by Version, since that is the shape of the globals Clang generates for a framework.

Add this to your Info.plist before anything else. It is what lets your app talk to the Frak wallet: without it, iOS answers “not installed” to every probe, so isFrakAppInstalled() returns false, the sharing sheet’s install detection never fires, and the handoff to the wallet silently degrades to the App Store every time.

Info.plist
<key>LSApplicationQueriesSchemes</key>
<array>
<string>frakwallet</string>
<string>frakwallet-dev</string>
</array>

List both: frakwallet is the production wallet and frakwallet-dev is the one you test against on a dev build. Declaring only production makes a locally built dev wallet undetectable, which is exactly the build a first integration runs against.

You also declare the URL scheme (or Universal Link) your own app receives referral links on. See Referral deep links below.

Call Frak.initialize once, at app startup:

import FrakSDK
@main
struct YourApp: App {
init() {
Frak.initialize(
FrakConfig(
merchantId: "your-merchant-id",
metadata: FrakMetadata(
name: "Your Store",
currency: .eur,
homepageLink: "https://your-store.com"
),
logLevel: .info
)
)
}
var body: some Scene { WindowGroup { ContentView() } }
}
OptionDefaultMeaning
merchantIdnilYour merchant ID from the business dashboard. When nil, the merchant is resolved from bundleId.
bundleIdnilFalls back to Bundle.main.bundleIdentifier.
metadataemptyname, currency, lang, logoURL, homepageLink.
deepLink.manual.manual or .disabled. iOS has no automatic mode, see below.
trackingEnabledtrueA hard floor. When false, no anonymous ID is ever minted.
logLevel.none.none, .error, .warn, .info, .debug.
logSinknilA FrakLogSink that replaces the default logger.

homepageLink is the last fallback of the share link chain: without it, a store-wide share with no product and no explicit link has nothing to point at.

Frak.client is throwing-synchronous, Frak.clientOrNull returns an optional instead. Every namespace member on the client is async.

private func client() -> FrakClient? { try? Frak.client }
let reward = await client()?.rewards.best(targetInteraction: "purchase")

The client exposes environment, anonymousId, resetAnonymousId(), setTrackingEnabled(_:), isTrackingEnabled(), and five namespaces: config, rewards, sharing, tracking, appLink.

The snippets below assume you already hold a client, for example with let client = try Frak.client.

// A confirmed order, on your order confirmation screen
let result = await client.tracking.purchase(
customerId: "cust_123",
orderId: "order_456",
token: "a-unique-order-token"
)
switch result {
case .success: print("order tracked")
case .failure(let error): print(error.localizedDescription)
}
// Any other interaction
await client.tracking.track(.custom("added_to_cart"))

Both calls return a Result rather than throwing, and both succeed once the event is durable, not once it is delivered. Events are written to a queue on disk and drained with retries, so an offline device still reports later.

Interaction is built through static factories:

Interaction.custom("checkout")
Interaction.custom("checkout", data: ["plan": "pro"])
Interaction.sharing()
Interaction.arrival(referrerWallet: nil, referrerClientId: nil, referrerMerchantId: nil, referralTimestamp: nil)

You rarely build arrival yourself: appLink.handleReferral does it for you.

let campaigns = try await client.rewards.campaigns()
let best = try await client.rewards.best(
targetInteraction: "purchase",
products: visibleProducts.map { ProductDetails(productId: $0.id, name: $0.title) }
)

Call best once per screen for the whole visible product set, not once per row: a single BestReward cannot be mapped back onto per-item rows, and the cache is keyed on the encoded product list. Both calls accept forceRefresh: true to skip the cache.

let resolved = try await client.config.resolve()
// Or observe changes
for await config in await client.config.updates {
// react to a refreshed merchant config
}

resolve is stale-while-revalidate, current returns the last resolved config without a call, and updates is a multicast stream that replays the latest value. See Backend-driven configuration.

let link = try await client.sharing.buildLink(
SharingRequest(
products: [
SharingProduct(
title: product.title,
link: product.link,
imageURL: product.imageURL,
utmContent: product.id,
details: ProductDetails(productId: product.id, name: product.title)
)
],
targetInteraction: "purchase",
placement: "product-page"
)
)

buildLink returns nil only when there is nothing to link to (no request link, no product link, and no homepage fallback). It throws a FrakError when a link should have been buildable but was not, for example when tracking is disabled.

AttributionParams (utmSource, utmMedium, utmCampaign, utmContent, utmTerm, via, ref) can be passed per call and merges over your merchant-level defaults.

FrakSDKUI adds a single view modifier:

import FrakSDKUI
struct ProductView: View {
@State private var isSharing = false
var body: some View {
Button("Share and earn") { isSharing = true }
.frakSharingSheet(isPresented: $isSharing, request: request) { result in
switch result {
case .shared(let link): print("shared \(link)")
case .copied(let link): print("copied \(link)")
case .installStarted: break // informational only
case .walletOpened: break // the wallet was installed and opened
case .dismissed: break
case .failed(let error): print(error.localizedDescription)
}
}
}
}

The callback fires once per presentation, with the most significant outcome. Ranked lowest to highest: failed, dismissed, shared and copied, installStarted, walletOpened. So a user who installs the wallet and then swipes the sheet away still reports the install, not the dismissal.

.frakSharingSheet(
isPresented: $isSharing,
request: request,
configuration: FrakSharingConfiguration(
heightFraction: 0.9,
install: .overlay(.init(position: .bottomRaised))
)
) { result in
// handle SharingResult
}
OptionDefaultMeaning
heightFraction0.85Sheet height, clamped to 0.3...1.0.
install.storeProductPage.storeProductPage raises a modal SKStoreProductViewController; .overlay shows an SKOverlay banner that does not cover the sheet.
detectInstalltruePolls for the wallet becoming installable while the store surface is up, then hands off and reports .walletOpened.

Install detection relies on the same LSApplicationQueriesSchemes entry that isFrakAppInstalled() needs. Without it, neither works.

iOS offers no automatic mode: nothing lets a library install itself in front of your app’s own URL routing. Wire it yourself:

.onOpenURL { url in
Task { await Frak.clientOrNull?.appLink.handleReferral(url) }
}

handleReferral returns whether the URL carried a Frak referral context. It is not a “stop routing” signal, so keep navigating either way.

The rest of the namespace:

await client.appLink.isFrakAppInstalled()
await client.appLink.openFrakApp() // .openedApp, .openedStore, or .failed
try await client.appLink.installPageURL(returnScheme: "yourapp", sessionId: sessionId)

Frak.parseReferralLink(_:) is static and pure, so you can decode a link before the SDK is initialized.

On top of the wallet schemes, declare the scheme your app receives referral links on:

Info.plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.your-company.your-app</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>yourapp</string>
</array>
</dict>
</array>

To receive https:// referral links instead of a custom scheme, add the Associated Domains capability and publish an apple-app-site-association file for your domain. That part is standard Universal Links setup and the SDK does not do it for you.

The SDK ships no consent UI. Wire your consent flow to:

await client.setTrackingEnabled(false) // stops tracking and purges anything queued
await client.resetAnonymousId() // rotates the local identity

Both products ship a PrivacyInfo.xcprivacy. It declares three collected data types, all linked to the user, none used for tracking:

Data typeWhat it covers
User IDThe anonymous ID and the customerId you pass to tracking.purchase.
Purchase historycustomerId, orderId, and the checkout token.
Product interactionArrival, sharing, and custom interactions.

NSPrivacyTracking is false: no ad network is in the SDK path.