Kotlin library for creator install attribution. On first install the SDK reads the Play Install Referrer and posts once to /ingestInstall. State is persisted locally so later launches reuse the attribution id and offer.
Add JitPack to your project settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}Then in the app module (0.3.3 or later):
implementation("com.github.tapapplink:tapapplink-android:0.3.3")Upgrading from 0.3.0? 0.3.1 changed applyCode on Android. See the changelog.
./gradlew needs JDK 17 or 21. JDK 25 is not supported by this Android Gradle Plugin.
TapAppLink.configure(
TapAppLinkConfig(
publicKey = "etk_live_…",
environment = TapAppLinkEnvironment.PRODUCTION,
debugLogging = BuildConfig.DEBUG,
)
)
// Call on every launch. Posts once per install; later launches return stored state.
TapAppLink.trackInstall(context) { result ->
val offer = TapAppLink.getOffer()
}
// Optional: override the Play Install Referrer if you already collected it.
TapAppLink.trackInstall(context, installReferrer) { }
TapAppLink.setAppUserId(Purchases.sharedInstance.appUserID) { }Show success only on a real success result. Map each outcome to UI like this. On a network or timeout error only (never unknown, inactive or wrongEnvironment), retry applyCode once automatically with the same code. From 0.3.2 that retry is safe because the SDK reuses its request ID, so the server won't count the install twice. From 0.3.3, success results may include optional codeMatches and offer.creatorName (see TapAppLinkRedeemResult).
fun redeem(code: String, isRetry: Boolean = false) {
TapAppLink.applyCode(code) { result ->
result.onSuccess { body ->
val offer = TapAppLink.getOffer()
val offerLine = offer?.let { "${it.creatorName}'s offer" }
if (body.optBoolean("alreadyAttributed")) {
// "You're all set" — hide the code; show offerLine if any.
showStatus(title = "You're all set", codeVisible = false, hint = offerLine)
} else {
// "Code applied" — show the code; show offerLine if any.
showStatus(title = "Code applied", codeVisible = true, code = code, hint = offerLine)
}
}.onFailure { error ->
when (error) {
is TapAppLinkRedeemException.UnknownCode -> {
showStatus(
title = "We don't recognise that code. Check it and try again.",
hint = "Codes aren't case sensitive.",
)
}
is TapAppLinkRedeemException.InactiveCode -> {
showStatus(
title = "This code is no longer active.",
hint = "You can still subscribe at the regular price.",
)
}
is TapAppLinkRedeemException.WrongEnvironment -> {
// Same customer copy as unknownCode. Never say "environment" to customers.
Log.w(TAG, TapAppLinkRedeemException.WrongEnvironment.DEVELOPER_WARNING)
showStatus(
title = "We don't recognise that code. Check it and try again.",
hint = "Codes aren't case sensitive.",
)
}
is TapAppLinkRedeemException.Network -> {
if (!isRetry) {
// Automatic retry once on network/timeout only.
redeem(code, isRetry = true)
} else {
showStatus(
title = "We couldn't check your code. Check your connection and try again.",
actionLabel = "Try again",
onAction = { redeem(code) },
)
}
}
is TapAppLinkRedeemException.Other -> {
Log.w(TAG, "applyCode failed status=${error.status} message=${error.message}")
showStatus(
title = "We couldn't check your code. Check your connection and try again.",
)
}
else -> {
Log.w(TAG, "applyCode failed", error)
showStatus(
title = "We couldn't check your code. Check your connection and try again.",
)
}
}
}
}
}
redeem(code)TapAppLinkRedeemException.WrongEnvironment.DEVELOPER_WARNING is exactly:
This code belongs to the other environment (Sandbox or Production). Check your API key.
trackInstall() is safe on every launch. It stores an installId, the tracked flag, attribution id and offer in SharedPreferences, and only posts /ingestInstall once per install. setAppUserId sends that stored attributionId after hydrate, posts /ingestIdentify only when the app user id changes, and coalesces concurrent identical calls.
Call resetForTesting(context) in debug builds before repeating a match test on the same device.
Purchases are attributed through billing webhooks. Leave out a client trackPurchase call.
GitHub Actions runs on every pull request and push to main: Gradle assemble, unit tests, ktlint, and Android Lint.
Pushing a semver tag (0.3.3, v0.3.3, or a prerelease suffix) runs the same checks, creates a GitHub Release, and requests a JitPack build for that tag. jitpack.yml pins OpenJDK 17 for JitPack.
./gradlew assembleRelease test ktlintCheck lintReleaseJitPack (current): consumers depend on com.github.tapapplink:tapapplink-android:0.3.3 (0.3.3 or later). Tag a release; CI triggers JitPack. No extra secrets are required for that path.
Maven Central (optional, disabled): the release workflow includes a maven-central job left behind if: false until a repository admin finishes the one-time setup below. Prefer JitPack until you need Central discovery or a non-com.github coordinate.
One-time setup (repository admin with write access to secrets and to the Sonatype / Central Portal account):
- Create a Maven Central Portal account and claim the
com.tapapplinknamespace (or your chosen groupId). - Generate a Portal user token (username + password pair).
- Create a GPG key for signing artefacts and export the private key (ASCII-armoured).
- In this GitHub repo, add Actions secrets:
MAVEN_CENTRAL_USERNAME– Portal token usernameMAVEN_CENTRAL_PASSWORD– Portal token passwordSIGNING_GPG_PRIVATE_KEY– ASCII-armoured private keySIGNING_GPG_PASSPHRASE– passphrase for that key
- Wire Gradle publishing to the Central Portal (for example via the
com.vanniktech.maven.publishplugin or the officialmaven-publish+ signing setup), then set themaven-centraljobifto run when those secrets are present.
Until steps 1–5 are done, leave the job disabled so tagged releases only ship via GitHub Releases and JitPack.
MIT