11package org.interdependency.ahbg
22
3+ import android.app.Activity
34import android.content.Context
45import com.revenuecat.purchases.CustomerInfo
6+ import com.revenuecat.purchases.Offerings
7+ import com.revenuecat.purchases.PurchaseParams
58import com.revenuecat.purchases.Purchases
6- import com.revenuecat.purchases.PurchasesError
79import com.revenuecat.purchases.PurchasesConfiguration
10+ import com.revenuecat.purchases.PurchasesError
11+ import com.revenuecat.purchases.interfaces.PurchaseCallback
812import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
13+ import com.revenuecat.purchases.interfaces.ReceiveOfferingsCallback
14+ import com.revenuecat.purchases.models.StoreTransaction
915
1016/* *
1117 * One clean entitlement: `benchmark_lab`.
@@ -16,30 +22,53 @@ import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
1622 * active for the current app user.
1723 *
1824 * RevenueCat 10.x core supports Google Play billing by default through the
19- * ordinary `PurchasesConfiguration`; no store module is required. The same
20- * entitlement boundary would apply unchanged to any later store module.
25+ * ordinary `PurchasesConfiguration`; no store module is required.
2126 *
22- * When the RevenueCat key is not provisioned (free/dev builds), the store
23- * degrades to the free tier rather than crashing or faking premium.
27+ * Usage: build the Play release with the app-specific RevenueCat public key
28+ * (`-PrevenueCatApiKey=goog_...`). The presentation bridge calls
29+ * `purchaseBenchmarkLab` and `restoreBenchmarkLab`; both return the resulting
30+ * entitlement state to the human-visible UI. Missing/invalid keys fail closed
31+ * to the free tier rather than entering a broken billing flow.
2432 */
33+ data class PremiumActionResult (
34+ val ok : Boolean ,
35+ val unlocked : Boolean ,
36+ val message : String ,
37+ )
38+
2539interface PremiumStore {
2640 fun isBenchmarkLabUnlocked (): Boolean
41+ fun purchaseBenchmarkLab (activity : Activity , callback : (PremiumActionResult ) -> Unit )
42+ fun restoreBenchmarkLab (callback : (PremiumActionResult ) -> Unit )
2743
2844 companion object {
2945 const val ENTITLEMENT = " benchmark_lab"
46+ const val PRODUCT_ID = " ahbg_benchmark_lab"
3047
3148 fun create (context : Context , revenueCatApiKey : String ): PremiumStore {
32- return if (revenueCatApiKey.isBlank() || revenueCatApiKey.startsWith(" REVENUECAT_KEY_NOT_PROVISIONED" )) {
33- NoopPremiumStore ()
34- } else {
35- RevenueCatPremiumStore (context, revenueCatApiKey)
49+ if (revenueCatApiKey.isBlank() || revenueCatApiKey.startsWith(" REVENUECAT_KEY_NOT_PROVISIONED" )) {
50+ return NoopPremiumStore (" RevenueCat Google Play key is not provisioned" )
51+ }
52+ if (! revenueCatApiKey.startsWith(" goog_" )) {
53+ return NoopPremiumStore (" RevenueCat Google Play public SDK key must start with goog_" )
3654 }
55+ return RevenueCatPremiumStore (context, revenueCatApiKey)
3756 }
3857 }
3958}
4059
41- class NoopPremiumStore : PremiumStore {
60+ class NoopPremiumStore (
61+ private val reason : String = " RevenueCat is not provisioned" ,
62+ ) : PremiumStore {
4263 override fun isBenchmarkLabUnlocked (): Boolean = false
64+
65+ override fun purchaseBenchmarkLab (activity : Activity , callback : (PremiumActionResult ) -> Unit ) {
66+ callback(PremiumActionResult (ok = false , unlocked = false , message = reason))
67+ }
68+
69+ override fun restoreBenchmarkLab (callback : (PremiumActionResult ) -> Unit ) {
70+ callback(PremiumActionResult (ok = false , unlocked = false , message = reason))
71+ }
4372}
4473
4574class RevenueCatPremiumStore (
@@ -54,10 +83,19 @@ class RevenueCatPremiumStore(
5483 Purchases .configure(
5584 PurchasesConfiguration .Builder (context.applicationContext, apiKey).build()
5685 )
86+ refreshCustomerInfo()
87+ }
88+
89+ private fun applyCustomerInfo (customerInfo : CustomerInfo ): Boolean {
90+ benchmarkLabUnlocked =
91+ customerInfo.entitlements.active[PremiumStore .ENTITLEMENT ]?.isActive == true
92+ return benchmarkLabUnlocked
93+ }
94+
95+ private fun refreshCustomerInfo () {
5796 Purchases .sharedInstance.getCustomerInfo(object : ReceiveCustomerInfoCallback {
5897 override fun onReceived (customerInfo : CustomerInfo ) {
59- benchmarkLabUnlocked =
60- customerInfo.entitlements.active[PremiumStore .ENTITLEMENT ]?.isActive == true
98+ applyCustomerInfo(customerInfo)
6199 }
62100
63101 override fun onError (error : PurchasesError ) {
@@ -67,4 +105,91 @@ class RevenueCatPremiumStore(
67105 }
68106
69107 override fun isBenchmarkLabUnlocked (): Boolean = benchmarkLabUnlocked
108+
109+ override fun purchaseBenchmarkLab (activity : Activity , callback : (PremiumActionResult ) -> Unit ) {
110+ Purchases .sharedInstance.getOfferings(object : ReceiveOfferingsCallback {
111+ override fun onReceived (offerings : Offerings ) {
112+ val packageToPurchase = offerings.current?.availablePackages?.firstOrNull {
113+ it.product.id == PremiumStore .PRODUCT_ID
114+ }
115+ if (packageToPurchase == null ) {
116+ callback(
117+ PremiumActionResult (
118+ ok = false ,
119+ unlocked = benchmarkLabUnlocked,
120+ message = " Current RevenueCat offering does not contain ${PremiumStore .PRODUCT_ID } " ,
121+ )
122+ )
123+ return
124+ }
125+
126+ val params = PurchaseParams .Builder (activity, packageToPurchase).build()
127+ Purchases .sharedInstance.purchase(params, object : PurchaseCallback {
128+ override fun onCompleted (storeTransaction : StoreTransaction , customerInfo : CustomerInfo ) {
129+ val unlocked = applyCustomerInfo(customerInfo)
130+ callback(
131+ PremiumActionResult (
132+ ok = unlocked,
133+ unlocked = unlocked,
134+ message = if (unlocked) {
135+ " Benchmark Lab purchase active"
136+ } else {
137+ " Purchase completed but benchmark_lab entitlement is not active"
138+ },
139+ )
140+ )
141+ }
142+
143+ override fun onError (error : PurchasesError , userCancelled : Boolean ) {
144+ callback(
145+ PremiumActionResult (
146+ ok = false ,
147+ unlocked = benchmarkLabUnlocked,
148+ message = if (userCancelled) " Purchase cancelled" else " Purchase failed: ${error.message} " ,
149+ )
150+ )
151+ }
152+ })
153+ }
154+
155+ override fun onError (error : PurchasesError ) {
156+ callback(
157+ PremiumActionResult (
158+ ok = false ,
159+ unlocked = benchmarkLabUnlocked,
160+ message = " Could not load Benchmark Lab offering: ${error.message} " ,
161+ )
162+ )
163+ }
164+ })
165+ }
166+
167+ override fun restoreBenchmarkLab (callback : (PremiumActionResult ) -> Unit ) {
168+ Purchases .sharedInstance.restorePurchases(object : ReceiveCustomerInfoCallback {
169+ override fun onReceived (customerInfo : CustomerInfo ) {
170+ val unlocked = applyCustomerInfo(customerInfo)
171+ callback(
172+ PremiumActionResult (
173+ ok = true ,
174+ unlocked = unlocked,
175+ message = if (unlocked) {
176+ " Benchmark Lab purchase restored"
177+ } else {
178+ " Restore completed; benchmark_lab entitlement is not active for this Play account"
179+ },
180+ )
181+ )
182+ }
183+
184+ override fun onError (error : PurchasesError ) {
185+ callback(
186+ PremiumActionResult (
187+ ok = false ,
188+ unlocked = benchmarkLabUnlocked,
189+ message = " Restore failed: ${error.message} " ,
190+ )
191+ )
192+ }
193+ })
194+ }
70195}
0 commit comments