iOS SDK
Section titled “iOS SDK”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.
Requirements
Section titled “Requirements”| Item | Value |
|---|---|
| Minimum iOS | 15 |
| Minimum Xcode | 16 (the package declares Swift 6 language mode) |
| Dependencies | None |
| License | Apache-2.0 |
Install
Section titled “Install”In Xcode: File → Add Package Dependencies, then enter https://github.com/frak-id/frak-ios-sdk.
Or in a 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"), ])]| Product | Contents |
|---|---|
FrakSDK | Identity, config, rewards, interaction tracking, sharing links, app links. No UI, no web view. |
FrakSDKUI | Adds 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.
Declare the wallet schemes
Section titled “Declare the wallet schemes”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.
<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.
Initialize
Section titled “Initialize”Call Frak.initialize once, at app startup:
import FrakSDK
@mainstruct 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() } }}Configuration options
Section titled “Configuration options”| Option | Default | Meaning |
|---|---|---|
merchantId | nil | Your merchant ID from the business dashboard. When nil, the merchant is resolved from bundleId. |
bundleId | nil | Falls back to Bundle.main.bundleIdentifier. |
metadata | empty | name, currency, lang, logoURL, homepageLink. |
deepLink | .manual | .manual or .disabled. iOS has no automatic mode, see below. |
trackingEnabled | true | A hard floor. When false, no anonymous ID is ever minted. |
logLevel | .none | .none, .error, .warn, .info, .debug. |
logSink | nil | A 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.
The client
Section titled “The client”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.
Tracking
Section titled “Tracking”// A confirmed order, on your order confirmation screenlet 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 interactionawait 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.
Rewards
Section titled “Rewards”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.
Configuration resolution
Section titled “Configuration resolution”let resolved = try await client.config.resolve()
// Or observe changesfor 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.
Sharing
Section titled “Sharing”Build a link yourself
Section titled “Build a link yourself”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.
The sharing sheet
Section titled “The sharing sheet”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.
Tuning the sheet
Section titled “Tuning the sheet”.frakSharingSheet( isPresented: $isSharing, request: request, configuration: FrakSharingConfiguration( heightFraction: 0.9, install: .overlay(.init(position: .bottomRaised)) )) { result in // handle SharingResult}| Option | Default | Meaning |
|---|---|---|
heightFraction | 0.85 | Sheet 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. |
detectInstall | true | Polls 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.
Referral deep links
Section titled “Referral deep links”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 .failedtry 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.
Info.plist
Section titled “Info.plist”On top of the wallet schemes, declare the scheme your app receives referral links on:
<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.
Consent and privacy
Section titled “Consent and privacy”The SDK ships no consent UI. Wire your consent flow to:
await client.setTrackingEnabled(false) // stops tracking and purges anything queuedawait client.resetAnonymousId() // rotates the local identityBoth products ship a PrivacyInfo.xcprivacy. It declares three collected data types, all linked to the user, none used for tracking:
| Data type | What it covers |
|---|---|
| User ID | The anonymous ID and the customerId you pass to tracking.purchase. |
| Purchase history | customerId, orderId, and the checkout token. |
| Product interaction | Arrival, sharing, and custom interactions. |
NSPrivacyTracking is false: no ad network is in the SDK path.