SDK / ANDROID

Android SDK.

Play Billing 8 with coroutines and Flow. RevFox reports every purchase to your server and tells you what each customer is entitled to. No paywall UI: you build the screen.

01

Requirements

  • Android 6.0 (API 23) or later, Kotlin 2.
  • A running RevFox server with a Play Store app and its service account credentials. See Dashboard setup.
  • The app's SDK API key (play_…) from API keys in the panel.
02

Install

The SDK is published on JitPack and brings Play Billing 8 with it.

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven("https://jitpack.io")
    }
}

// app/build.gradle.kts
dependencies {
    implementation("com.github.revfox-dev:revfox-android:0.1.0")
}
03

Configure

Initialize once, from Application.onCreate:

import com.revfox.RevFox
import com.revfox.RevFoxConfiguration
import com.revfox.RevFoxLogLevel

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) RevFox.logLevel = RevFoxLogLevel.DEBUG
        RevFox.shared.initialize(
            this,
            RevFoxConfiguration(
                apiKey = "play_…",
                baseUrl = "https://api.example.com",
            ),
        )
    }
}
ParameterDefaultDescription
apiKeyrequiredThe app's SDK API key.
baseUrlrequiredYour RevFox API server, the api_url of your config. Must be https.
customerIdnullYour user ID, when someone is already signed in at launch. null starts anonymously.
transactionFinisherAUTOMATICAUTOMATIC: RevFox runs purchases and acknowledges or consumes them. MANUAL: observer mode.
allowInsecurefalseAllows an http base URL. For local development only.
environmentnullWhich entitlements the build reads. null means SANDBOX for debuggable builds and PRODUCTION otherwise. Set it for a release build that license testers use.
consumableProductIdsemptyOne-time products that can be bought again. Play does not say which products are consumable, so RevFox consumes these and acknowledges everything else.

initialize throws a RevFoxError for an invalid configuration, or already_initialized when called twice. On launch it reports any purchase an earlier run could not, and loads the customer. It re-reads the customer each time the app returns to the foreground.

04

How Play maps onto RevFox

  • Product IDs. A Play subscription is sold through base plans, so each base plan is its own product, with the ID productId:basePlanId. That is also the ID RevFox records purchases under. fetchProducts(listOf("pro")) returns every base plan of pro; fetchProducts(listOf("pro:monthly")) returns one. One-time products keep their plain ID.
  • Offers. Play lists only the offers a customer is eligible for. The first one on a base plan is its introductoryOffer, and purchase applies it unless you pass another. Tag an offer revfox-ignore in Play Console to keep it out.
  • Consumables. List them in consumableProductIds.
05

Products and packages

Fetch a package you set up in the panel. RevFox gives its Play product IDs; titles, prices and offers come from Play Billing.

val pkg = RevFox.shared.fetchPackage("default")
pkg.products.forEach { println("${it.title} ${it.displayPrice}") }

val products = RevFox.shared.fetchProducts(listOf("pro:monthly", "pro:yearly"))

RevFoxProduct has the same fields as on iOS: id, kind, title, description, price (a BigDecimal), displayPrice, currencyCode, subscriptionPeriod, introductoryOffer, promotionalOffers and packageId. IDs Play does not know are left out.

06

Purchase

when (val result = RevFox.shared.purchase(activity, product)) {
    is RevFoxPurchaseResult.Success -> unlock(result.customer.benefits)
    RevFoxPurchaseResult.UserCancelled -> Unit
    RevFoxPurchaseResult.Pending -> showPendingMessage() // arrives later through customerUpdates
}

Once Play approves a purchase, purchase returns Success even if your server could not be reached. The purchase stays unacknowledged, so the next launch reports it. A purchase is never reported as failed because of the network.

  • offer is one of the product's offers. It defaults to the product's introductoryOffer.
  • obfuscatedAccountId ties the purchase to your account. For an anonymous customer it defaults to the generated customer ID. It must not contain personal data.
  • Buying a product fetched through a package records that package as presented_package_id.
07

Check benefits

Check benefits, not product IDs. customerUpdates is a Flow that starts with the cached customer and emits every change:

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        RevFox.shared.customerUpdates.collect { customer ->
            isPro = "pro" in customer.benefits
        }
    }
}

RevFox.shared.currentCustomer is the latest snapshot and currentCustomerId the ID in use. RevFoxCustomer and RevFoxPurchaseStatus have the same fields as on iOS; dates are java.util.Date.

08

Sign in and sign out

// When someone signs in. Anything bought anonymously moves to them.
val customer = RevFox.shared.setCustomer("8f14e45f-ceea-4672-9a1f-2a1b0c3d4e5f")

// When they sign out. Benefits stop being reported at once.
RevFox.shared.clearCustomer()

setCustomer switches only once your server has accepted it; if it throws, calling it again later carries the same purchases over. clearCustomer throws already_anonymous when nobody is signed in.

09

Restore purchases

val customer = RevFox.shared.restorePurchases()

Links the purchases Play reports for the device's Google account to the current customer. A customer with no purchases means there was nothing to restore. Calling it again is safe. It never acknowledges or consumes.

10

Observer mode

Keep your own BillingClient. Set transactionFinisher = RevFoxTransactionFinisher.MANUAL and report each purchase once its state is PURCHASED:

override fun onPurchasesUpdated(result: BillingResult, purchases: List<Purchase>?) {
    purchases?.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED }?.forEach { purchase ->
        scope.launch {
            RevFox.shared.submitTransaction(purchase)
            acknowledgeOrConsume(purchase) // still your code's job
        }
    }
}

RevFox then never acknowledges or consumes anything. submitTransaction throws purchase_pending for a purchase that is not paid yet.

11

Attributes

The SDK reports platform, OS, app version, locale, time zone and Play country by itself. Anything personal is sent only when you set it; null clears a value.

RevFox.shared.setEmail("[email protected]")
RevFox.shared.setDisplayName("Ada Lovelace")
RevFox.shared.setPushToken(fcmToken)
RevFox.shared.setAttributes(mapOf("plan" to "team"))

RevFox.shared.setMediaSource("Meta")
RevFox.shared.setAdjustID(Adjust.getAdid())

The attribution and integration ID setters are the same as on iOS.

collectDeviceIdentifiers() sends the device model, manufacturer and OS version and has your server record the IP address. When your app includes com.google.android.gms:play-services-ads-identifier and the user has not opted out of ad personalization, it also sends the Google advertising ID. Setting an attribution tool's install ID does the same unless automaticDeviceIdentifierCollectionEnabled is false.

12

Logging

RevFox.logLevel = RevFoxLogLevel.DEBUG // VERBOSE, DEBUG, INFO (default), WARN, ERROR
RevFox.logHandler = RevFoxLogHandler { level, message -> Timber.tag("RevFox").d("$level $message") }

By default, messages go to Logcat under the RevFox tag.

13

Errors

Failures are RevFoxError exceptions with a stable code. Most codes are the same as on iOS, without the iOS-only keychain_error and transaction_unverified. These are Android's own:

CodeMeaning
purchase_in_progressAnother purchase is already open.
product_already_purchasedThe customer already owns this product.
offer_not_foundThe customer is not eligible for the offer passed to purchase.
purchase_pendingsubmitTransaction got a purchase that is not paid yet.

See the full list on the iOS page.

14

Privacy

For Google Play's Data safety form: the SDK always sends purchase history (Financial info), the customer ID (Personal info → User IDs) and app details such as version and locale (App info and performance), to your server over HTTPS, for app functionality. Device model, OS version and IP address are only collected after collectDeviceIdentifiers().

SetterData safety category
setEmail, setDisplayName, setPhoneNumberPersonal info → Email address, Name, Phone number.
setPushToken, setAdvertisingIdentifier, collectDeviceIdentifiers(), integration IDsDevice or other IDs.
Attribution settersApp activity → Other actions.

Building with Flutter?

One Dart API for both platforms.

Flutter SDK