Use this skill when selling one time products on Android with RevenueCat. Covers fetching offerings, running awaitPurchase on a non subscription package, and relying on the SDK to acknowledge or consume automatically.
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Sell one time products on Android without writing BillingClient glue. The RevenueCat SDK fetches products, launches the purchase, verifies the receipt, and acknowledges or consumes the token for you.
Answer these before touching code.
| Question | Decision |
|---|---|
| Is the product consumable or non consumable? | One time products are consumed by default. Mark lifetime or permanent unlocks as non consumable in the RevenueCat dashboard (Product catalog > Products). |
| Do you read access through entitlements or transactions? | Prefer entitlements so access logic stays decoupled from product IDs. |
| Do you need server notification of the purchase? | Use RevenueCat webhooks. The NON_RENEWING_PURCHASE event fires for every new non subscription purchase. |
Do not call BillingClient.queryProductDetailsAsync, acknowledgePurchase, or consumeAsync yourself once RevenueCat is integrated. The SDK owns the BillingClient instance.
Purchases.configure runs once on app start with your Android API key and the current App User ID.Fetch offerings, launch the purchase, then read the result off CustomerInfo.
val offerings = Purchases.sharedInstance.awaitOfferings()
val pkg = offerings.current?.availablePackages?.firstOrNull() ?: returnLaunch the purchase with awaitPurchase. It suspends until the RevenueCat backend verifies the token.
val result = Purchases.sharedInstance.awaitPurchase(
PurchaseParams.Builder(activity, pkg).build()
)
val info = result.customerInfoRead access through entitlements, or fall back to the non subscription transactions list.
val hasAccess = info.entitlements["lifetime_access"]?.isActive == true
val owned = info.nonSubscriptionTransactions
.any { it.productIdentifier == "lifetime_product_id" }The SDK acknowledges non consumables and consumes consumables automatically once the backend verifies the purchase. You do not call acknowledgePurchase or consumeAsync.
Wrap awaitPurchase to separate cancellation, pending payments, and real errors.
try { /* awaitPurchase */ } catch (e: PurchasesTransactionException) {
when {
e.error.code == PurchasesErrorCode.PaymentPendingError -> showPending()
e.userCancelled -> Unit
else -> showError(userFacingMessage(e.error)) // log e.error.message; mapping in rc-error-handling
}
}| Outcome | SDK signal | Action |
|---|---|---|
| Success | awaitPurchase returns with updated CustomerInfo | Grant access from entitlements or nonSubscriptionTransactions. |
| User cancel | PurchasesTransactionException, userCancelled == true | Swallow silently. |
| Pending payment | PurchasesTransactionException, PaymentPendingError | Show a pending message. The SDK updates the entitlement later through UpdatedCustomerInfoListener. |
| Other error | PurchasesTransactionException, other codes | Log e.error.message and show a mapped string from userFacingMessage (see rc-error-handling). |
result.customerInfo from awaitPurchase. Do not grant access from optimistic local state.UpdatedCustomerInfoListener so you react when the payment completes.NON_RENEWING_PURCHASE webhook. With Google Play Real Time Developer Notifications connected to RevenueCat, a refund of a one time purchase arrives as CANCELLATION. Do not reimplement Google Play Developer API verification.RevenueCat/ai-toolkit
2aeae8c
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.