How to Restore In-App Purchases on Android
Introduction
When users reinstall your app or switch to a new Android device, they may need to restore their
previously purchased subscriptions. RevenueCat's restorePurchases() method re-syncs
the user's Google Play purchase history with RevenueCat's servers and updates the
CustomerInfo object with any active entitlements.
Common scenarios where restore is needed on Android:
- User reinstalls the app after clearing app data or factory resetting
- User moves to a new device and installs the app for the first time
- User's RevenueCat customer record was reset or the app user ID changed
- Purchase state appears incorrect and needs to be re-synced from Google Play
How Android Restore Works
Android subscriptions are tied to a Google account, not to a device. When a user signs in to Google Play with the same account that made the original purchase, Google Play automatically grants them access to their subscriptions. This differs from iOS, where restoration requires an explicit App Store receipt fetch.
RevenueCat's restorePurchases() goes one step further: it queries Google Play's billing
library for the full purchase history, validates the purchases with Google's servers, and syncs the
result to the user's RevenueCat record. This resolves edge cases where the automatic Google Play
restoration did not propagate to RevenueCat.
Basic Implementation with Coroutines (Recommended)
The cleanest approach uses Kotlin coroutines with awaitRestorePurchases(), which is
the suspend function variant of the API:
// In a Fragment or Activity
restoreButton.setOnClickListener {
restoreButton.isEnabled = false
lifecycleScope.launch {
try {
val customerInfo = Purchases.sharedInstance.awaitRestorePurchases()
if (customerInfo.entitlements["premium"]?.isActive == true) {
showSuccessMessage("Purchases restored successfully!")
updateUIForPremium()
} else {
showMessage("No active purchases found. Contact support if you believe this is an error.")
}
} catch (e: PurchasesException) {
Log.e("Purchases", "Restore failed: ${e.message}")
showErrorMessage("Failed to restore: ${e.message}")
} finally {
restoreButton.isEnabled = true
}
}
}
awaitRestorePurchases() throws a PurchasesException on failure.
The exception's code property contains a PurchasesErrorCode enum value
that lets you distinguish between network errors, billing errors, and other failure types.
} catch (e: PurchasesException) {
val userMessage = when (e.code) {
PurchasesErrorCode.NetworkError ->
"Network error. Please check your connection and try again."
PurchasesErrorCode.StoreProblemError ->
"Google Play is unavailable. Please try again later."
else ->
"Restore failed: ${e.message}"
}
showErrorMessage(userMessage)
}
Callback-based Implementation
For codebases that are not yet using coroutines, the callback-based API is available with
restorePurchasesWith():
restoreButton.isEnabled = false
Purchases.sharedInstance.restorePurchasesWith(
onSuccess = { customerInfo ->
restoreButton.isEnabled = true
if (customerInfo.entitlements["premium"]?.isActive == true) {
updateUI(isPremium = true)
Toast.makeText(this, "Purchases restored!", Toast.LENGTH_SHORT).show()
} else {
showNoPurchasesDialog()
}
},
onError = { error ->
restoreButton.isEnabled = true
Log.e("Purchases", "Restore failed: ${error.message}")
showErrorDialog(error.message)
}
)
Both callbacks are dispatched on the main thread, so you can safely update UI elements directly
without wrapping in runOnUiThread.
Jetpack Compose Implementation
For apps using Jetpack Compose, the recommended pattern is a ViewModel that manages
the restore flow and exposes state as a StateFlow:
// ViewModel
class SubscriptionViewModel : ViewModel() {
private val _isRestoring = MutableStateFlow(false)
val isRestoring = _isRestoring.asStateFlow()
private val _restoreResult = MutableStateFlow<RestoreResult?>(null)
val restoreResult = _restoreResult.asStateFlow()
fun restorePurchases() {
viewModelScope.launch {
_isRestoring.value = true
_restoreResult.value = null
try {
val customerInfo = Purchases.sharedInstance.awaitRestorePurchases()
_restoreResult.value = if (customerInfo.entitlements["premium"]?.isActive == true) {
RestoreResult.Success("Your purchases have been restored!")
} else {
RestoreResult.NoPurchases("No active purchases found.")
}
} catch (e: PurchasesException) {
_restoreResult.value = RestoreResult.Error("Restore failed: ${e.message}")
} finally {
_isRestoring.value = false
}
}
}
}
sealed class RestoreResult {
data class Success(val message: String) : RestoreResult()
data class NoPurchases(val message: String) : RestoreResult()
data class Error(val message: String) : RestoreResult()
}
// Composable
@Composable
fun RestorePurchasesButton(
viewModel: SubscriptionViewModel = viewModel()
) {
val isRestoring by viewModel.isRestoring.collectAsState()
val restoreResult by viewModel.restoreResult.collectAsState()
var showDialog by remember { mutableStateOf(false) }
// Show result dialog when restore completes
LaunchedEffect(restoreResult) {
if (restoreResult != null) showDialog = true
}
Button(
onClick = { viewModel.restorePurchases() },
enabled = !isRestoring
) {
if (isRestoring) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
Text("Restoring...")
} else {
Text("Restore Purchases")
}
}
if (showDialog) {
AlertDialog(
onDismissRequest = {
showDialog = false
viewModel.clearRestoreResult()
},
title = { Text("Restore Purchases") },
text = {
when (val result = restoreResult) {
is RestoreResult.Success -> Text(result.message)
is RestoreResult.NoPurchases -> Text(result.message)
is RestoreResult.Error -> Text(result.message)
null -> { }
}
},
confirmButton = {
TextButton(onClick = {
showDialog = false
viewModel.clearRestoreResult()
}) {
Text("OK")
}
}
)
}
}
Add the clearRestoreResult() function to the ViewModel:
fun clearRestoreResult() {
_restoreResult.value = null
}
Android vs iOS: Key Differences
If you are building for both platforms, be aware of these behavioral differences:
| Behavior | Android (Google Play) | iOS (App Store) |
|---|---|---|
| Purchases tied to | Google account | Apple ID |
| Auto-restore on reinstall | Yes (via Google Play) | No (requires explicit call) |
| Restore button required | Recommended, not mandated | Required by App Store guidelines |
| RevenueCat method | awaitRestorePurchases() |
restorePurchases() |
Important Notes
restorePurchases() triggers a
Google Play billing query and a RevenueCat API call. Calling it automatically on every launch is
unnecessary (Google Play handles active subscriptions automatically) and will slow down your app.
Only call it when the user explicitly requests a restore.
restorePurchases() will return empty entitlements even if they have
active subscriptions. Show a prompt to sign in to Google Play if entitlements are unexpectedly empty.
restorePurchases() will not find the
original purchase. The user must be signed in to the same Google account that made the original
purchase. Encourage users to check their Google account in the Play Store.
Related Guides
- Full Android In-App Purchases Tutorial — End-to-end RevenueCat Android integration
- Restore Purchases on iOS (Swift) — The iOS equivalent of this guide
- Troubleshooting — Common RevenueCat issues and solutions