Skip to content
This repository was archived by the owner on Oct 17, 2025. It is now read-only.

Commit bef7d3a

Browse files
committed
refactor(android): remove redundant purchase casts
1 parent bb8ddbd commit bef7d3a

File tree

5 files changed

+59
-34
lines changed

5 files changed

+59
-34
lines changed

Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package dev.hyo.martie.screens
22

33
import android.app.Activity
4-
import android.content.Context
54
import androidx.compose.foundation.background
65
import androidx.compose.foundation.layout.*
76
import androidx.compose.foundation.lazy.LazyColumn
@@ -30,12 +29,12 @@ import dev.hyo.openiap.store.PurchaseResultStatus
3029
import kotlinx.coroutines.delay
3130
import kotlinx.coroutines.flow.first
3231
import kotlinx.coroutines.launch
33-
import dev.hyo.openiap.Product
3432
import dev.hyo.openiap.ProductAndroid
3533
import dev.hyo.openiap.ProductQueryType
3634
import dev.hyo.openiap.ProductType
3735
import dev.hyo.openiap.Purchase
3836
import dev.hyo.openiap.PurchaseAndroid
37+
import dev.hyo.martie.util.findActivity
3938

4039
@OptIn(ExperimentalMaterial3Api::class)
4140
@Composable
@@ -44,16 +43,22 @@ fun PurchaseFlowScreen(
4443
storeParam: OpenIapStore? = null
4544
) {
4645
val context = LocalContext.current
47-
val activity = context as? Activity
46+
val activity = remember(context) { context.findActivity() }
4847
val uiScope = rememberCoroutineScope()
49-
val appContext = context.applicationContext as Context
48+
val appContext = remember(context) { context.applicationContext }
5049
val iapStore = storeParam ?: remember(appContext) { OpenIapStore(appContext) }
5150
val products by iapStore.products.collectAsState()
5251
val purchases by iapStore.availablePurchases.collectAsState()
5352
val androidProducts = remember(products) { products.filterIsInstance<ProductAndroid>() }
5453
val androidPurchases = remember(purchases) { purchases.filterIsInstance<PurchaseAndroid>() }
5554
val status by iapStore.status.collectAsState()
5655
val lastPurchase by iapStore.currentPurchase.collectAsState(initial = null)
56+
val lastPurchaseAndroid: PurchaseAndroid? = remember(lastPurchase) {
57+
when (val purchase = lastPurchase) {
58+
is PurchaseAndroid -> purchase
59+
else -> null
60+
}
61+
}
5762
val connectionStatus by iapStore.connectionStatus.collectAsState()
5863
val clipboard = LocalClipboardManager.current
5964
val statusMessage = status.lastPurchaseResult
@@ -202,17 +207,17 @@ fun PurchaseFlowScreen(
202207
) {
203208
OutlinedButton(
204209
onClick = {
205-
(lastPurchase as? PurchaseAndroid)?.let { p ->
210+
lastPurchaseAndroid?.let { p ->
206211
val json = p.toJson().toString()
207212
clipboard.setText(AnnotatedString(json))
208213
}
209214
},
210-
enabled = lastPurchase is PurchaseAndroid
215+
enabled = lastPurchaseAndroid != null
211216
) { Text("Copy Result") }
212217
Spacer(modifier = Modifier.width(8.dp))
213218
OutlinedButton(
214-
onClick = { selectedPurchase = lastPurchase as? PurchaseAndroid },
215-
enabled = lastPurchase is PurchaseAndroid
219+
onClick = { lastPurchaseAndroid?.let { selectedPurchase = it } },
220+
enabled = lastPurchaseAndroid != null
216221
) { Text("Details") }
217222
}
218223
}
@@ -344,8 +349,8 @@ fun PurchaseFlowScreen(
344349

345350
// Auto-handle purchase: validate on server then finish
346351
// IMPORTANT: Implement real server-side receipt validation in validateReceiptOnServer()
347-
LaunchedEffect(lastPurchase?.id) {
348-
val purchase = lastPurchase as? PurchaseAndroid ?: return@LaunchedEffect
352+
LaunchedEffect(lastPurchaseAndroid?.id) {
353+
val purchase = lastPurchaseAndroid ?: return@LaunchedEffect
349354
try {
350355
// 1) Server-side validation (replace with your backend call)
351356
val valid = validateReceiptOnServer(purchase)

Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import androidx.compose.ui.Alignment
1616
import androidx.compose.ui.Modifier
1717
import androidx.compose.ui.platform.LocalContext
1818
import android.app.Activity
19-
import android.content.Context
2019
import androidx.compose.ui.text.font.FontWeight
2120
import androidx.compose.ui.unit.dp
2221
import androidx.navigation.NavController
@@ -34,6 +33,7 @@ import dev.hyo.openiap.store.PurchaseResultStatus
3433
import dev.hyo.openiap.OpenIapError
3534
import kotlinx.coroutines.launch
3635
import kotlinx.coroutines.delay
36+
import dev.hyo.martie.util.findActivity
3737

3838
// Helper to format remaining time like "3d 4h" / "2h 12m" / "35m"
3939
private fun formatRemaining(deltaMillis: Long): String {
@@ -56,9 +56,9 @@ fun SubscriptionFlowScreen(
5656
storeParam: OpenIapStore? = null
5757
) {
5858
val context = LocalContext.current
59-
val activity = context as? Activity
59+
val activity = remember(context) { context.findActivity() }
6060
val uiScope = rememberCoroutineScope()
61-
val appContext = context.applicationContext as Context
61+
val appContext = remember(context) { context.applicationContext }
6262
val iapStore = storeParam ?: remember(appContext) { OpenIapStore(appContext) }
6363
val products by iapStore.products.collectAsState()
6464
val purchases by iapStore.availablePurchases.collectAsState()
@@ -67,6 +67,12 @@ fun SubscriptionFlowScreen(
6767
val status by iapStore.status.collectAsState()
6868
val connectionStatus by iapStore.connectionStatus.collectAsState()
6969
val lastPurchase by iapStore.currentPurchase.collectAsState(initial = null)
70+
val lastPurchaseAndroid: PurchaseAndroid? = remember(lastPurchase) {
71+
when (val purchase = lastPurchase) {
72+
is PurchaseAndroid -> purchase
73+
else -> null
74+
}
75+
}
7076

7177
// Real-time subscription status (expiry/renewal). This requires server validation.
7278
data class SubscriptionUiInfo(
@@ -99,14 +105,16 @@ fun SubscriptionFlowScreen(
99105
}
100106

101107
// Refresh server-side status when purchases change
102-
LaunchedEffect(purchases) {
108+
LaunchedEffect(androidPurchases) {
103109
val map = mutableMapOf<String, SubscriptionUiInfo>()
104-
purchases
110+
androidPurchases
105111
.filter { it.productId in IapConstants.SUBS_SKUS }
106-
.forEach { p ->
107-
val token = p.purchaseToken ?: return@forEach
108-
val info = fetchSubStatusFromServer(p.productId, token)
109-
if (info != null) map[p.productId] = info.copy(autoRenewing = p.isAutoRenewing)
112+
.forEach { purchase ->
113+
val token = purchase.purchaseToken ?: return@forEach
114+
val info = fetchSubStatusFromServer(purchase.productId, token)
115+
if (info != null) {
116+
map[purchase.productId] = info.copy(autoRenewing = purchase.isAutoRenewing)
117+
}
110118
}
111119
subStatus = map
112120
}
@@ -253,7 +261,7 @@ fun SubscriptionFlowScreen(
253261

254262
// Active Subscriptions Section
255263
// Treat any purchase with matching subscription SKU as subscribed
256-
val activeSubscriptions = androidPurchases.filter { it.productId in IapConstants.SUBS_SKUS }
264+
val activeSubscriptions = androidPurchases.filter { it.productId in IapConstants.SUBS_SKUS }
257265
if (activeSubscriptions.isNotEmpty()) {
258266
item {
259267
SectionHeaderView(title = "Active Subscriptions")
@@ -383,8 +391,8 @@ fun SubscriptionFlowScreen(
383391
return true
384392
}
385393

386-
LaunchedEffect(lastPurchase?.id) {
387-
val purchase = lastPurchase as? PurchaseAndroid ?: return@LaunchedEffect
394+
LaunchedEffect(lastPurchaseAndroid?.id) {
395+
val purchase = lastPurchaseAndroid ?: return@LaunchedEffect
388396
try {
389397
// 1) Server-side validation (replace with your backend call)
390398
val valid = validateReceiptOnServer(purchase)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package dev.hyo.martie.util
2+
3+
import android.app.Activity
4+
import android.content.Context
5+
import android.content.ContextWrapper
6+
7+
tailrec fun Context.findActivity(): Activity? = when (this) {
8+
is Activity -> this
9+
is ContextWrapper -> baseContext.findActivity()
10+
else -> null
11+
}

openiap/src/main/java/dev/hyo/openiap/OpenIapModule.kt

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class OpenIapModule(private val context: Context) : PurchasesUpdatedListener {
7878
private var currentActivityRef: WeakReference<Activity>? = null
7979
private val productManager = ProductManager()
8080
private val gson = Gson()
81+
private val fallbackActivity: Activity? = if (context is Activity) context else null
8182

8283
private val purchaseUpdateListeners = mutableSetOf<OpenIapPurchaseUpdateListener>()
8384
private val purchaseErrorListeners = mutableSetOf<OpenIapPurchaseErrorListener>()
@@ -146,15 +147,15 @@ class OpenIapModule(private val context: Context) : PurchasesUpdatedListener {
146147

147148
val getActiveSubscriptions: QueryGetActiveSubscriptionsHandler = { subscriptionIds ->
148149
withContext(Dispatchers.IO) {
149-
val purchases = queryPurchases(billingClient, BillingClient.ProductType.SUBS)
150-
val filtered = if (subscriptionIds.isNullOrEmpty()) {
151-
purchases
150+
val androidPurchases = queryPurchases(billingClient, BillingClient.ProductType.SUBS)
151+
.filterIsInstance<PurchaseAndroid>()
152+
val ids = subscriptionIds.orEmpty()
153+
val filtered = if (ids.isEmpty()) {
154+
androidPurchases
152155
} else {
153-
purchases.filter { purchase ->
154-
(purchase as? PurchaseAndroid)?.productId?.let(subscriptionIds::contains) == true
155-
}
156+
androidPurchases.filter { it.productId in ids }
156157
}
157-
filtered.mapNotNull { (it as? PurchaseAndroid)?.toActiveSubscription() }
158+
filtered.map { it.toActiveSubscription() }
158159
}
159160
}
160161

@@ -165,7 +166,7 @@ class OpenIapModule(private val context: Context) : PurchasesUpdatedListener {
165166
val requestPurchase: MutationRequestPurchaseHandler = { props ->
166167
val purchases = withContext(Dispatchers.IO) {
167168
val androidArgs = props.toAndroidPurchaseArgs()
168-
val activity = currentActivityRef?.get() ?: (context as? Activity)
169+
val activity = currentActivityRef?.get() ?: fallbackActivity
169170

170171
if (activity == null) {
171172
val err = OpenIapError.MissingCurrentActivity

openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class OpenIapStore(private val module: OpenIapModule) {
111111

112112
// Expose a way to set the current Activity for purchase flows
113113
fun setActivity(activity: Activity?) {
114-
(module as? OpenIapModule)?.setActivity(activity)
114+
module.setActivity(activity)
115115
}
116116

117117
init {
@@ -175,9 +175,9 @@ class OpenIapStore(private val module: OpenIapModule) {
175175
is FetchProductsResultSubscriptions -> {
176176
val subs = result.value.orEmpty()
177177
_subscriptions.value = subs
178-
_products.value = subs.mapNotNull { subscription ->
179-
(subscription as? ProductSubscriptionAndroid)?.toProduct()
180-
}
178+
_products.value = subs
179+
.filterIsInstance<ProductSubscriptionAndroid>()
180+
.map { it.toProduct() }
181181
}
182182
}
183183
result

0 commit comments

Comments
 (0)