Skip to content

The Frak Android SDK brings referral tracking, rewards, and the sharing sheet to a native Android app. It is written in Kotlin, callable from Java, and ships as two artifacts so an app that only needs tracking never links a web view.

ItemValue
Minimum SDK24 (Android 7.0)
Java / JVM target17
LanguageKotlin 2.2 language level, Java call sites supported
LicenseApache-2.0

Two artifacts, released together:

ArtifactContents
id.frak.sdk:coreIdentity, config, rewards, interaction tracking, sharing links, app links. No UI, no web view.
id.frak.sdk:uiThe sharing sheet. Depends on core.
app/build.gradle.kts
dependencies {
implementation("id.frak.sdk:core:1.0.0-beta.1")
// Only if you show the sharing sheet
implementation("id.frak.sdk:ui:1.0.0-beta.1")
}

Artifacts are published to Maven Central. Pin an exact version: if your build cannot resolve the coordinates above, ask the Frak team for the current release.

The SDK’s own manifest declares the INTERNET permission and the <queries> entries needed to detect the Frak wallet app, so you do not add either by hand. It declares no activity and no intent filter: your app keeps ownership of its own deep links.

Call Frak.initialize once, in Application.onCreate or in your launcher Activity’s onCreate after super.onCreate.

Frak.initialize(
context = applicationContext,
config = FrakConfig(merchantId = BuildConfig.FRAK_MERCHANT_ID) {
metadata = FrakMetadata {
name = "Your Store"
currency = FrakCurrency.EUR
homepageLink = "https://your-store.com"
}
deepLink = DeepLinkHandling.Automatic
logLevel = FrakLogLevel.INFO
},
)

The Kotlin trailing-lambda form is sugar over the same Builder, not a second implementation.

OptionDefaultMeaning
merchantIdnullYour merchant ID from the business dashboard. When null, the merchant is resolved from packageId instead.
packageIdnullFalls back to context.packageName.
metadataemptyStatic merchant facts: name, currency, lang, logoUrl, homepageLink.
deepLinkAutomaticAutomatic, Manual, or Disabled. See Referral deep links.
trackingEnabledtrueA hard floor. Setting it to false cannot be lifted at runtime.
logLevelNONENONE, ERROR, WARN, INFO, DEBUG.
logSinknullA FrakLogSink that replaces logcat. Must be thread-safe and must not throw.

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 throws if the SDK is not initialized; Frak.clientOrNull returns null instead. Every namespace member is a suspend function.

lifecycleScope.launch {
val reward = Frak.client.rewards.best(
RewardRequest { targetInteraction = "purchase" },
)
}

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

Every suspending member has a CompletableFuture twin named *Async. The work runs on the SDK’s IO dispatcher and the future completes on the main thread, so a continuation can touch a View directly.

Frak.getClient().getRewards()
.bestAsync(new RewardRequest.Builder().targetInteraction("purchase").build())
.thenAccept(reward -> banner.setText(reward == null ? "" : reward.getFormatted()));
// A confirmed order, on your order confirmation screen
when (val result = Frak.client.tracking.purchase(
customerId = "cust_123",
orderId = "order_456",
token = "a-unique-order-token",
)) {
is FrakResult.Success -> Log.i("frak", "order tracked")
is FrakResult.Failure -> Log.w("frak", result.error.message)
}
// Any other interaction
Frak.client.tracking.track(Interaction.custom("added_to_cart"))

track and purchase succeed once the event is durable, not once it is delivered. Events are queued on disk oldest-first and retried, so an offline device still reports later.

Interaction is an opaque type built through static factories:

Interaction.custom("checkout")
Interaction.custom("checkout", mapOf("plan" to "pro"))
Interaction.sharing()
Interaction.arrival(referrerWallet, referrerClientId, referrerMerchantId, referralTimestamp)

You rarely build arrival yourself: appLink.handleReferral does it for you, and building a second one for the same link double-counts the arrival.

val campaigns = Frak.client.rewards.campaigns()
val best = Frak.client.rewards.best(
RewardRequest {
targetInteraction = "purchase"
products = visibleProducts.map { product ->
ProductDetails {
productId = product.id
name = product.title
}
}
},
)

Call best once per screen for the whole visible product set, not once per row: the cache is keyed on the encoded product list. Both calls accept a forceRefresh overload that skips the cache and the backoff.

val config = Frak.client.config.resolve()
config.displayName
config.displayLogoUrl

resolve is stale-while-revalidate with a five minute freshness window, so calling it on every screen is cheap. The resolved config is a read model: you read placements, component copy, and translations from it, and never construct one. See Backend-driven configuration.

val link = Frak.client.sharing.buildLink(
SharingRequest {
products = listOf(
SharingProduct(title = product.title, link = product.link) {
imageUrl = product.imageUrl
utmContent = product.id
details = ProductDetails {
productId = product.id
name = product.title
unitPrice = product.priceCents / 100.0
}
},
)
targetInteraction = "purchase"
placement = "product-page"
},
)

buildLink returns null only when there is nothing to link to (no link, no product link, and no homepageLink). 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 set per call and merges field by field over your merchant-level defaults.

The id.frak.sdk:ui artifact adds a ready-made sheet. Its whole public surface is FrakSharing, SharingResult, and FrakSharingDefaults.

private lateinit var sharing: FrakSharing
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// In onCreate, after super.onCreate. Not a property initializer: an Activity
// has no ViewModelStore before that, and build() throws without one.
sharing = FrakSharing.Builder(::onShareResult).build(this)
}
// When a share affordance becomes visible
sharing.warm()
// On the tap
sharing.present(SharingRequest { targetInteraction = "purchase" })

Builder.heightFraction(Float) tunes the sheet height, between 0.3 and 1.0, defaulting to 0.85. warm() is cheap to call repeatedly, and present implies it.

The sheet reports exactly once per session, through your callback:

private fun onShareResult(result: SharingResult) {
when (result) {
is SharingResult.Shared -> Log.i("frak", "shared ${result.link}")
is SharingResult.Copied -> Log.i("frak", "copied ${result.link}")
SharingResult.InstallStarted -> Unit // informational only
SharingResult.WalletOpened -> Unit // the wallet app was already installed and opened
SharingResult.Dismissed -> Unit
is SharingResult.Failed -> Log.w("frak", result.error.message)
}
}

InstallStarted is informational: it does not mean anything was installed, and it is not a cue to call openFrakApp again.

DeepLinkHandling.Automatic (the default) registers an activity lifecycle observer that reads the inbound intent and calls handleReferral for you. Choose Manual to call it yourself, or Disabled to opt out entirely.

Your app still declares its own intent filters for the domain or scheme you send referral links to:

AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<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" android:host="your-store.com" android:pathPrefix="/product" />
</intent-filter>
</activity>

The appLink namespace covers the rest:

Frak.client.appLink.handleReferral(url) // decode, guard self-referral, track the arrival
Frak.client.appLink.isFrakAppInstalled() // synchronous, no async twin
Frak.client.appLink.openFrakApp() // opens the wallet, or the Play Store listing
Frak.client.appLink.installPageUrl(returnScheme, sessionId)

handleReferral returns whether a referral context was found. It is not a “stop routing” signal: your own navigation still runs.

Test an inbound link without a real referral:

Terminal window
adb shell am start -a android.intent.action.VIEW \
-d "https://your-store.com/product?fCtx=test_token_123" your.app.id

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

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

For Play Data Safety, three things leave the device and only three:

WhatPlay data type
Anonymous ID and the customerId you pass to tracking.purchasePersonal info, User IDs
Referral and sharing eventsApp activity, App interactions
customerId, orderId, and the checkout tokenFinancial info, Purchase history

No advertising ID, no ANDROID_ID, no install referrer, no location, no contacts. The anonymous ID is a per-install keypair held in the Android Keystore, non-exportable, and gone on uninstall, which is why it is declared as a user ID rather than a device ID.

Two caveats worth knowing before you build a compliance story on setTrackingEnabled: the decision is written asynchronously, so a withdrawal lost to a process kill reverts to enabled on the next launch, and the web SDK has no equivalent switch today.

Frak.shutdown() cancels background work and unregisters the deep-link observer, after which initialize can run again. It is not a consent control: it records no decision.