Skip to content

Commit cdd6bc9

Browse files
fix(ahbg): wire Play purchase and restore
1 parent dca3152 commit cdd6bc9

1 file changed

Lines changed: 137 additions & 12 deletions

File tree

Lines changed: 137 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
package org.interdependency.ahbg
22

3+
import android.app.Activity
34
import android.content.Context
45
import com.revenuecat.purchases.CustomerInfo
6+
import com.revenuecat.purchases.Offerings
7+
import com.revenuecat.purchases.PurchaseParams
58
import com.revenuecat.purchases.Purchases
6-
import com.revenuecat.purchases.PurchasesError
79
import com.revenuecat.purchases.PurchasesConfiguration
10+
import com.revenuecat.purchases.PurchasesError
11+
import com.revenuecat.purchases.interfaces.PurchaseCallback
812
import 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+
2539
interface 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

4574
class 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

Comments
 (0)