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.
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")
}
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",
),
)
}
}
| Parameter | Default | Description |
|---|---|---|
apiKey | required | The app's SDK API key. |
baseUrl | required | Your RevFox API server, the api_url of your config. Must be https. |
customerId | null | Your user ID, when someone is already signed in at launch. null starts anonymously. |
transactionFinisher | AUTOMATIC | AUTOMATIC: RevFox runs purchases and acknowledges or consumes them. MANUAL: observer mode. |
allowInsecure | false | Allows an http base URL. For local development only. |
environment | null | Which entitlements the build reads. null means SANDBOX for debuggable builds and PRODUCTION otherwise. Set it for a release build that license testers use. |
consumableProductIds | empty | One-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.
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 ofpro;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, andpurchaseapplies it unless you pass another. Tag an offerrevfox-ignorein Play Console to keep it out. - Consumables. List them in
consumableProductIds.
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.
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.
offeris one of the product's offers. It defaults to the product'sintroductoryOffer.obfuscatedAccountIdties 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.
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.
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.
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.
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.
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.
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.
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:
| Code | Meaning |
|---|---|
purchase_in_progress | Another purchase is already open. |
product_already_purchased | The customer already owns this product. |
offer_not_found | The customer is not eligible for the offer passed to purchase. |
purchase_pending | submitTransaction got a purchase that is not paid yet. |
See the full list on the iOS page.
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().
| Setter | Data safety category |
|---|---|
setEmail, setDisplayName, setPhoneNumber | Personal info → Email address, Name, Phone number. |
setPushToken, setAdvertisingIdentifier, collectDeviceIdentifiers(), integration IDs | Device or other IDs. |
| Attribution setters | App activity → Other actions. |
