SubSovereign op Android — integratiehandleiding
Deze handleiding brengt je Android-app (telefoon, tablet, Android TV of Amazon Fire TV) van "Ik weet niet wie mij betaald heeft" naar "mijn app ontgrendelt de juiste functies voor de juiste gebruiker, geverifieerd op mijn eigen server." De handleiding is geschreven om van begin tot eind te volgen — er wordt geen voorkennis van abonnementsgereedschap verondersteld. Elke Kotlin-codevoorbeeld hierin is gecompileerd tegen de SDK bij elke testrun, dus wat je hier leest is wat de code doet.
Wat SubSovereign voor je doet
Je gebruikers abonneren zich via de Google Play-winkel (of Amazon op Fire TV). SubSovereign beantwoordt één vraag voor je app, betrouwbaar: waar heeft deze gebruiker daadwerkelijk voor betaald?
- Je app vraagt SubSovereign naar het toegangsrecht van de gebruiker — de toegang die ze hebben ontgrendeld.
- De ontvangstbevestiging gebeurt op de server, rechtstreeks met de winkel, zodat een aangepaste app geen abonnement kan vervalsen.
- De paywall — het scherm dat je abonnementen aanbiedt — wordt op afstand geconfigureerd en getekend door de SDK, zodat je prijzen, proefperiodes en formuleringen kunt wijzigen zonder een nieuwe app-versie uit te brengen.
- De EU-herroepingsfunctie — de wettelijk vereiste annuleringsknop — wordt ook door de SDK getekend, in de taal van de klant, zodat een wettelijke melding niet kan vervagen tot marketing.
- Het is zelf-gehost: SubSovereign draait op jouw infrastructuur, de gegevens van je gebruikers blijven bij jou, en er is geen inkomstenverdeling — je houdt 100% van wat je gebruikers betalen.
Je vertrouwt nooit op de telefoon. De telefoon vraagt; de server beslist.
Voordat je begint
Je hebt nodig:
- Een draaiende SubSovereign-server (je zelf-gehoste implementatie). Je richt de SDK hierop. Als je er nog geen hebt, implementeer deze eerst — de rest van deze handleiding gaat ervan uit dat deze live is.
- Een app geregistreerd in het dashboard. Maak in het SubSovereign-dashboard een app aan. Je krijgt twee dingen:
- een
appId(identificeert je app), en - een API-sleutel (het inloggegeven dat je app gebruikt om met de server te communiceren). Registreer deze voor het platform waarop het wordt uitgebracht — telefoon, Android TV of Fire TV. Eén codebase die op meerdere platforms draait is prima; zie Telefoons en televisies onder Stap 2.
- een
- Toegangsniveaus aangemaakt. Definieer in het dashboard de toegangsniveaus (ook wel tier genoemd) die je app verleent — bijvoorbeeld
proofpremium— en koppel elk aan de winkelproduct-ID's die je gebruikers kopen. - Winkelbetalingen al werkend. SubSovereign valideert en volgt aankopen; het vervangt niet Google Play Billing (of Amazon IAP). Stel deze normaal in je app in — SubSovereign zit net na de aankoop om deze te verifiëren en vast te leggen.
Aan de codekant heb je Kotlin nodig met coroutines (de SDK-aanroepen zijn suspend-functies), Jetpack Compose (de paywall en de annuleringsknop zijn composables), en minSdk 24 of hoger.
Stap 1 — Voeg de SDK toe
De SDK wordt momenteel als broncode geleverd: er is nog niets te downloaden vanuit Maven Central. Neem de map sdk-android/src/main/java uit de SubSovereign-repository over in je project en compileer deze als onderdeel van je app — precies zoals de voorbeeld-app in dezelfde repository doet. Hieronder staat wat de SDK zelf nodig heeft, bovenop wat je app al heeft: de twee plugins die de code gebruikt, en de eigen afhankelijkheden vastgezet op de versies waarmee deze is gebouwd en getest (de laatste regel is wat het voorbeeld gebruikt om een composable op het scherm te zetten). Gebruik je AGP 8, pas dan ook org.jetbrains.kotlin.android toe; bij AGP 9 is Kotlin ingebouwd. Declareer de pluginversies in je root-buildbestand, zoals het voorbeeld doet.
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.plugin.compose")
id("org.jetbrains.kotlin.plugin.serialization") // the SDK's models are @Serializable
}
android {
// ... your existing settings; minSdk 24 or higher
buildFeatures { compose = true }
// Compile the SDK's source folder as part of your app — wherever you put it.
sourceSets["main"].kotlin.srcDir("../subsovereign-sdk/src/main/java")
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
implementation("io.coil-kt:coil-compose:2.7.0") // hero images — the SDK's one UI dependency
implementation(platform("androidx.compose:compose-bom:2024.09.02"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.runtime:runtime")
implementation("androidx.activity:activity-compose:1.9.3") // setContent, as in the sample
}
Zorg ervoor dat je app het internetrecht heeft in AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
Omdat de SDK als je eigen broncode wordt gecompileerd, is er geen versie om bij te werken: om deze te updaten, vervang je de map. Zodra er een gepubliceerd artefact bestaat, wordt deze stap één afhankelijkheidsregel en zal deze handleiding dat vermelden.
Stap 2 — Configureer één keer, bij het starten van je app
Configureer de SDK één keer — de beste plek is je Application.onCreate(), of direct nadat de gebruiker inlogt. Je geeft het je API-sleutel, je app-ID, een stabiele identifier voor deze gebruiker, hun taal, en op welk platform de app draait.
import com.subsovereign.sdk.SubSovereign
import com.subsovereign.sdk.SubSovereignConfig
import com.subsovereign.sdk.detectPlatform
SubSovereign.configure(
SubSovereignConfig(
apiKey = "YOUR_APP_API_KEY", // from the dashboard
appId = "your-app-id", // from the dashboard
baseUrl = "https://subs.yourdomain.com/api/v1", // YOUR self-hosted server
userId = currentUser.id, // your own stable user id
locale = customerLanguage, // the CUSTOMER's language, e.g. "fr" — decides the words they get
platform = detectPlatform(this), // "android", "androidtv" or "firetv"
)
)
Een paar dingen die het weten waard zijn:
userIdis van jou. Gebruik elke stabiele ID die je al hebt voor een ingelogde gebruiker. Gebruik steeds dezelfde waarde zodat de toegang van de gebruiker hen volgt over apparaten. Ondersteun je uitloggen en logt een andere gebruiker in, roep dan gewoonconfigureopnieuw aan met de nieuweuserId.baseUrlwijst naar jouw server. De standaardwaarde in de SDK is een placeholder — stel deze in op je eigen SubSovereign-implementatie.localeis de taal van de klant, niet die van je console, en wordt HIER bepaald: de server retourneert de paywall en de woorden van de wettelijke melding in de taal die je configureert, en de SDK formatteert datums hiervoor. Haal deze uit het profiel van de klant of van het apparaat, niet uit een letterlijke waarde.
Telefoons en televisies
Eén Play-codebase kan naar telefoons, tablets en Android TV worden gebracht; een Fire TV-build wordt via Amazon verspreid. platform vertelt de SDK op welk platform deze draait — "android", "androidtv" of "firetv" — en dat bepaalt drie dingen: welke paywall de server stuurt (een televisie-paywall is ingedeeld voor een kamer, niet voor een hand), het platform waaronder een annulering wordt geregistreerd, en waar je klanten vandaan komen. detectPlatform(context) bepaalt dit tijdens runtime: een Fire TV via Amazons eigen apparaatfunctie, een Android TV via de configuratie of Leanback, alles wat overblijft als telefoon. Hardcode "android" niet in een build die ook op televisies draait — dan wordt de telefoon-paywall op de TV opgehaald.
Op Fire TV moet je de app als Fire TV registreren in het dashboard, of een Fire TV-paywall publiceren. Een Fire TV-build vraagt de server om de Fire TV-paywall, en een server die deze niet heeft, antwoordt met een Error in plaats van de telefoon-paywall (de server valt terug van Android TV naar de telefoon-paywall, en van Fire TV naar niets). De SDK raadt nooit een platform: een platform-waarde die het niet herkent, is een Error die deze benoemt, en er wordt niets verzonden.
Stap 3 — Controleer wat de gebruiker kan benaderen
De snelle ja/nee-vraag is hasAccess(). Het faalt gesloten: bij een netwerkstoring, een serverfout of een SDK die niet is geconfigureerd, antwoordt het met false, zodat een tijdelijke storing nooit een betaalde functie kan ontgrendelen. Gebruik het op het punt waar je een functie afschermt:
lifecycleScope.launch {
if (SubSovereign.hasAccess("pro")) unlockProFeatures() else showPaywall()
}
hasAccess("pro") is alleen waar als een actief toegangsrecht die toegangsniveaunaam draagt; hasAccess() zonder naam is het algemene oordeel van de server.
Als je de details nodig hebt — wat precies actief is, wanneer het verlengt, of om een "nee" te onderscheiden van een "kon niet controleren" — roep dan checkEntitlements() aan. Het retourneert een resultaat waar jij over beslist, en de juiste beslissing bij een fout is meestal om de klant te houden waar ze waren:
import com.subsovereign.sdk.SubSovereignResult
import kotlinx.coroutines.launch
lifecycleScope.launch {
when (val result = SubSovereign.checkEntitlements()) {
is SubSovereignResult.Success -> {
val ent = result.data
if (ent.hasAccess) unlockProFeatures() else showPaywall()
}
is SubSovereignResult.Error -> {
// A network hiccup or a server error. Keep the customer on whatever access they
// last had and try again later — never lock a paying customer out over a blip.
Log.w("MyApp", "Entitlement check failed: ${result.message}")
}
}
}
Als je app meer dan één tier heeft, kijk dan in result.data.entitlements — elk daarvan vertelt je precies welk toegangsniveau actief is. Vergelijk op de naam die je aan het niveau in het dashboard hebt gegeven; accessLevelId is de interne id van de server, niet die naam:
val isPro = result.data.entitlements.any { it.isActive && it.accessLevelName.equals("pro", ignoreCase = true) }
Elk toegangsrecht bevat ook expiresAt, willRenew, en de store waar het vandaan komt — handig voor meldingen zoals "je abonnement verlengt op…". fromCache betekent dat de server antwoordde vanuit zijn eigen kortstondige cache in plaats van zijn database; het is nooit een offline-antwoord — een onbereikbare server is een Error, en deze SDK bewaart geen kopie.
Stap 4 — Verkoop een abonnement
Haal de paywall op
Haal de paywall van de server op in plaats van prijzen hard te coderen in je app. Dit stelt je in staat om een aanbieding te doen of de lengte van een proefperiode te wijzigen zonder een release:
lifecycleScope.launch {
when (val result = SubSovereign.getPaywallConfig()) {
is SubSovereignResult.Success -> showPaywall(result.data) // the paywall published for THIS surface
is SubSovereignResult.Error -> showFallbackPaywall() // your own built-in default
}
}
PaywallConfig bevat de kop, de functies, de producten die worden aangeboden (elk met een weergaveprijs, periode, proefversie en een optionele badge), de call-to-action, de voettekst, de kleuren en de sjabloon — alles wat het scherm nodig heeft.
Teken het
De SDK tekent de paywall. PaywallView is een composable: geef het de configuratie en vertel wat het moet doen wanneer de klant een product kiest.
import androidx.compose.runtime.Composable
@Composable
fun PaywallScreen(config: PaywallConfig, onBuy: (String) -> Unit) {
PaywallView(
config = config,
onDismiss = { /* the customer closed it — only reachable when you allow closing in the console */ },
) { productId -> onBuy(productId) } // start Google Play Billing (or Amazon IAP) for this product
}
Het regelt de lay-out, de kleuren die je in de console hebt gekozen (en zorgt ervoor dat de tekst leesbaar blijft, zelfs als een vervaging deze vaag zou maken), de sluitknop als je deze hebt toegestaan, en — op een televisie — de ring die laat zien welke besturing de afstandsbediening heeft geselecteerd. Je start de aankoopstroom van de winkel voor het product dat het je geeft. Het op het scherm plaatsen heeft de gebruikelijke Compose-vorm:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
class PaywallActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { PaywallScreen(config = paywall) { productId -> startPurchase(productId) } } // `paywall`: the PaywallConfig you fetched above
}
}
Voltooi de aankoop en verifieer deze
Voer de aankoop uit via Google Play Billing precies zoals je normaal zou doen. Wanneer Google je een purchaseToken teruggeeft, geef deze dan door aan SubSovereign zodat de server de ontvangst direct bij de winkel kan valideren en het toegangsniveau kan verlenen. Het tier komt uit de product-naar-niveau-mapping in je dashboard, nooit uit de app — een aangepaste app kan zichzelf niet upgraden — dus accessLevelId wordt geaccepteerd voor compatibiliteit en genegeerd:
lifecycleScope.launch {
val ok = SubSovereign.validateGooglePurchase(
purchaseToken = purchase.purchaseToken, // from Google Play Billing
productId = purchase.products.first(),
accessLevelId = "pro", // required by the SDK; the server grants the tier YOUR DASHBOARD maps this product to
)
when (ok) {
is SubSovereignResult.Success ->
// Confirmed by the server: re-check (fail-closed) and unlock on the answer, not on hope.
if (ok.data && SubSovereign.hasAccess("pro")) unlockProFeatures() else showTryAgain("Not confirmed yet")
is SubSovereignResult.Error -> showTryAgain(ok.message)
}
}
Dat is het hele vertrouwensmodel: de aankoop is pas echt als de server deze heeft bevestigd.
Op Amazon Fire TV
Fire TV-apps gebruiken Amazon's In-App Purchasing in plaats van Google Play. Het is hetzelfde patroon met andere invoer: Amazon geeft je een ontvangstbewijs en een gebruikersgegevens-object, en het ontvangstbewijs is alleen geldig samen met Amazons eigen gebruikers-ID — wat niet jouw userId is. De server koppelt de SKU aan het toegangsniveau, dus er is geen accessLevelId om door te geven:
SubSovereign.validateAmazonPurchase(
receiptId = receipt.receiptId, // from Amazon's PurchaseResponse
amazonUserId = userData.userId, // from Amazon's UserData — NOT your own userId
productId = receipt.sku,
)
Stap 5 — De EU-herroepingsfunctie (Compliance Passport)
Als je abonnementen verkoopt aan EU-consumenten, vereist Richtlijn (EU) 2023/2673 een duidelijk gelabelde herroepingsfunctie — de annuleringsknop. De SDK levert deze kant-en-klaar, en deze wordt in twee delen getekend: haal de instellingen voor je app op, en teken vervolgens de knop met deze instellingen.
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun CancelSubscription(subscriptionId: String, customerLocale: String) {
var settings by remember { mutableStateOf<WithdrawalConfig?>(null) }
LaunchedEffect(Unit) {
when (val r = SubSovereign.getWithdrawalConfig()) {
is SubSovereignResult.Success -> settings = r.data
// Log it: a silent failure here is how you ship English to German customers and never find out.
is SubSovereignResult.Error -> Log.w("MyApp", "Withdrawal settings unavailable: ${r.message}")
}
}
CancelButton(subscriptionId, settings, customerLocale)
}
@Composable
fun CancelButton(subscriptionId: String, settings: WithdrawalConfig?, customerLocale: String) {
if (settings?.enabled == false) return // switched off for this app in the console
// The WORDS are not passed in and cannot be: getWithdrawalConfig() fetched them in your
// customer's language and the SDK kept them, and this control reads them from there.
// Fetch FIRST — without it the notice falls back to English for every customer.
WithdrawalView(
subscriptionId = subscriptionId,
labelKey = settings?.labelKey, // ← the label chosen for this app
locale = customerLocale, // ← without this the DATE is formatted for the device
appearance = Appearance.AUTO, // LIGHT, DARK or AUTO — and nothing else
)
}
Je kunt de formulering niet wijzigen. Er zijn twee labels voor de herroepingsfunctie — "Herroep hier het contract" en "Hier opzeggen" — en labelKey bevat degene waarvoor je app is ingesteld; de woorden zelf komen van de server in de taal van de klant. Er is geen manier om je eigen tekst door te geven: een label-parameter accepteerde vroeger vrije tekst en wordt nu genegeerd met een gelogde waarschuwing. Een wettelijke melding die kan worden herschreven, kan worden verzacht tot marketing, wat dezelfde reden is waarom je geen kleur kunt doorgeven.
🚩 Geef strings door, anders is de wettelijke melding in het Engels voor iedereen. De woorden worden geleverd door jouw SubSovereign-server in de talen die deze ondersteunt; een taal die deze niet heeft, komt terug in het Engels. Welke taal je krijgt, wordt bepaald door de locale die je in Stap 2 hebt geconfigureerd — de instellingenaanroep stuurt deze door — dus customerLocale hier moet dezelfde waarde zijn; het formatteert de datum, het kiest niet de woorden. De knop geeft weer wat het krijgt en haalt de woorden niet zelf op, omdat hetzelfde contract op elk platform moet gelden, inclusief een platform waar een UI-component helemaal geen netwerk kan gebruiken. Als het ophalen mislukt, verschijnt de knop nog steeds, in het Engels, in plaats van niet te verschijnen: het niet tonen van een vindbare herroepingsfunctie aan de consument is jouw overtreding; het tonen ervan in de verkeerde taal is dat niet. Log de fout; laat deze nooit aan de klant zien.
🚩 Geef ook locale door. Het formatteert de tijdstempel op de bevestiging. Zonder deze valt de SDK terug op de locale die je hebt geconfigureerd; zonder beide wordt de datum in een vorm weergegeven die geen lezer verkeerd kan begrijpen. Een Duitse klant op een apparaat met Amerikaanse locale mag niet 9/3/2026 te zien krijgen, wat zij als 9 maart zullen lezen.
Uiterlijk — LIGHT, DARK of AUTO (de standaard), en niets anders. De knop tekent zijn eigen kaart en eigen tekst, zodat deze leesbaar is op elk scherm, licht of donker. AUTO volgt de donkere-modusinstelling van het apparaat op een telefoon of tablet en is altijd donker op een televisie. Er is met opzet geen manier om een kleur door te geven.
Het toont de gelabelde knop, bevestigt één keer, verzendt idempotent (een netwerkherhaling kan nooit twee herroepingen creëren), en toont de bevestiging met de datum. Het afschermen op enabled is aan jou, zoals hierboven; de knop zelf doet geen netwerk buiten het verzenden.
Stap 6 — Toestemming en privacy
Alles wat een privacyverzoek nodig heeft, is één aanroep per stuk, en allemaal gaan ze via je eigen server:
SubSovereign.recordConsent(purpose = "analytics", granted = true)
SubSovereign.setDoNotSell(enabled = true) // CCPA "do not sell" / Global Privacy Control
SubSovereign.requestErasure() // GDPR Art. 17 — the customer's right to be forgotten
val export = SubSovereign.exportMyData() // GDPR Art. 20 — their data, as JSON
val flags = SubSovereign.getFeatureFlags() // remote feature flags for this app
SubSovereign.recordAttributionTouch(utmSource = "newsletter") // where this customer first came from
De purpose van recordConsent is één van analytics, marketing, personalisation of consumption_data_sharing — al het andere is een Error; jurisdiction is standaard "GDPR" en policyVersion is standaard "1.0", geef je eigen waarden door als deze verschillen. Elk retourneert een SubSovereignResult zoals elke andere aanroep.
Werken met resultaten
Elke SDK-aanroep behalve hasAccess() retourneert een SubSovereignResult, wat ofwel Success (met de gegevens) of Error (met een bericht en, waar relevant, een HTTP-status code) is. Dit is met opzet: netwerkaanroepen falen, en je app moet beslissen wat te doen in plaats van vast te lopen. Een eenvoudige gewoonte:
- Bij
Success— gebruik de gegevens. - Bij
Error— log deze, houd de gebruiker op hun laatst bekende toegang, en probeer het later opnieuw. Sluit nooit een betalende gebruiker buiten vanwege een tijdelijke netwerkstoring. hasAccess()is de uitzondering: het is een gewoneBooleanen antwoordt metfalsebij elke fout, dus gebruik het om af te schermen encheckEntitlements()om uit te leggen.
Best practices
- Controleer bij koude start. Roep
checkEntitlements()aan wanneer de app start, zodat de afscherming correct is voordat de gebruiker bij een vergrendelde functie komt. - Controleer opnieuw na aankoop. Direct na een succesvolle
validate…-aanroep, voercheckEntitlements()opnieuw uit, zodat de UI de nieuwe toegang direct weerspiegelt. - Vertrouw nooit op de client. Bewaar "is pro" niet in de app en behandel het als waarheid. Vraag het aan de server; de server heeft de ontvangst geverifieerd.
- Eén
userIdper echte gebruiker. Houd deze stabiel zodat de toegang de gebruiker volgt over apparaten, en configureer opnieuw wanneer de ingelogde gebruiker verandert. - Laat de SDK het platform detecteren.
detectPlatform(context)bij het configureren, nooit een letterlijke waarde in een build die naar meer dan één type apparaat wordt gebracht. - Houd coroutines netjes. Dit zijn
suspend-functies — roep ze aan vanuit eenlifecycleScopeofviewModelScopezodat ze annuleren met het scherm.
Snelle referentie
| Je wilt… | Aanroep |
|---|---|
| De SDK instellen | SubSovereign.configure(config) met platform = detectPlatform(context) |
| Een functie afschermen, fail-closed | hasAccess(toegangsniveaunaam?) → Boolean |
| Zien wat de gebruiker heeft ontgrendeld | checkEntitlements() → EntitlementResult |
| De paywall voor dit platform ophalen | getPaywallConfig() → PaywallConfig |
| De paywall tekenen | PaywallView(config, onDismiss) { productId -> … } |
| Een Google Play-aankoop verifiëren | validateGooglePurchase(purchaseToken, productId, accessLevelId) |
| Een Amazon (Fire TV)-aankoop verifiëren | validateAmazonPurchase(receiptId, amazonUserId, productId) |
| De herroepingsinstellingen en -woorden ophalen | getWithdrawalConfig() → WithdrawalConfig |
| De EU-herroepingsfunctie tekenen | WithdrawalView(subscriptionId, strings, labelKey, locale, appearance) |
| GDPR-toestemming vastleggen | recordConsent(purpose, granted) |
| CCPA do-not-sell · wissen · exporteren · vlaggen · attributie | setDoNotSell · requestErasure · exportMyData · getFeatureFlags · recordAttributionTouch |
Volgende stappen
- Doe hetzelfde op je andere platforms — de iOS, Web/JavaScript en Roku SDK's volgen dezelfde vorm.
- Nieuw bij de concepten? Lees Hoe SubSovereign werkt.