Android SDK
Section titled “Android SDK”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.
Requirements
Section titled “Requirements”| Item | Value |
|---|---|
| Minimum SDK | 24 (Android 7.0) |
| Java / JVM target | 17 |
| Language | Kotlin 2.2 language level, Java call sites supported |
| License | Apache-2.0 |
Install
Section titled “Install”Two artifacts, released together:
| Artifact | Contents |
|---|---|
id.frak.sdk:core | Identity, config, rewards, interaction tracking, sharing links, app links. No UI, no web view. |
id.frak.sdk:ui | The sharing sheet. Depends on core. |
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.
Initialize
Section titled “Initialize”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 },)Frak.initialize( getApplicationContext(), new FrakConfig.Builder(BuildConfig.FRAK_MERCHANT_ID) .metadata(new FrakMetadata.Builder() .name("Your Store") .currency(FrakCurrency.EUR) .homepageLink("https://your-store.com") .build()) .deepLink(DeepLinkHandling.Automatic) .logLevel(FrakLogLevel.INFO) .build());The Kotlin trailing-lambda form is sugar over the same Builder, not a second implementation.
Configuration options
Section titled “Configuration options”| Option | Default | Meaning |
|---|---|---|
merchantId | null | Your merchant ID from the business dashboard. When null, the merchant is resolved from packageId instead. |
packageId | null | Falls back to context.packageName. |
metadata | empty | Static merchant facts: name, currency, lang, logoUrl, homepageLink. |
deepLink | Automatic | Automatic, Manual, or Disabled. See Referral deep links. |
trackingEnabled | true | A hard floor. Setting it to false cannot be lifted at runtime. |
logLevel | NONE | NONE, ERROR, WARN, INFO, DEBUG. |
logSink | null | A 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.
The client
Section titled “The client”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.
Calling from Java
Section titled “Calling from Java”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()));Tracking
Section titled “Tracking”// A confirmed order, on your order confirmation screenwhen (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 interactionFrak.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.
Rewards
Section titled “Rewards”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.
Configuration resolution
Section titled “Configuration resolution”val config = Frak.client.config.resolve()config.displayNameconfig.displayLogoUrlresolve 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.
Sharing
Section titled “Sharing”Build a link yourself
Section titled “Build a link yourself”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 sharing sheet
Section titled “The sharing sheet”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 visiblesharing.warm()
// On the tapsharing.present(SharingRequest { targetInteraction = "purchase" })val sharing = remember { FrakSharing.Builder(::onShareResult) }.build()
Button(onClick = { sharing.present(SharingRequest { targetInteraction = "purchase" })}) { Text("Share and earn")}The Compose build() warms the sheet on composition-enter, so there is no warm() call to place yourself.
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.
Referral deep links
Section titled “Referral deep links”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:
<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 arrivalFrak.client.appLink.isFrakAppInstalled() // synchronous, no async twinFrak.client.appLink.openFrakApp() // opens the wallet, or the Play Store listingFrak.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:
adb shell am start -a android.intent.action.VIEW \ -d "https://your-store.com/product?fCtx=test_token_123" your.app.idConsent and privacy
Section titled “Consent and privacy”The SDK ships no consent UI. Wire your consent platform to:
Frak.client.setTrackingEnabled(false) // stops tracking and purges anything queuedFrak.client.resetAnonymousId() // rotates the local identityFor Play Data Safety, three things leave the device and only three:
| What | Play data type |
|---|---|
Anonymous ID and the customerId you pass to tracking.purchase | Personal info, User IDs |
| Referral and sharing events | App activity, App interactions |
customerId, orderId, and the checkout token | Financial 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.
Shutting down
Section titled “Shutting down”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.