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.

Prerequisites: The RevenueCat Android SDK must be installed and configured with your Google Play API key. See the Android codelab for setup instructions. The user must be signed in to Google Play on the device for restore to work.

Basic Implementation with Coroutines (Recommended)

The cleanest approach uses Kotlin coroutines with awaitRestorePurchases(), which is the suspend function variant of the API:

kotlin
// 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.

kotlin
} 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():

kotlin
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:

kotlin
// 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()
}
kotlin
// 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:

kotlin
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

Do not call on every app launch. 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.
User must be signed in to Google Play. If the user is not signed in to Google Play on the device, 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.
Transferring purchases between accounts. If a user purchased on one Google account and is now on a different one, RevenueCat's 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