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

Commit 4736255

Browse files
authored
Fix AllProductsScreen and product fetching logic (#11)
## Summary - Fixed AllProductsScreen connection and retry logic - Improved product fetching to properly handle InApp and Subs types separately - Enhanced OpenIapStore to merge products instead of replacing them ## Changes - Added proper connection initialization with activity context in AllProductsScreen - Fixed product fetching by using separate calls for InApp and Subs types instead of ProductQueryType.All - Updated OpenIapStore fetchProducts to merge products with existing ones to prevent data loss - Implemented unified product display with type badges ('in-app' and 'subs') - Added proper cleanup on screen disposal ## Test plan - [x] Build and run the app - [x] Navigate to AllProductsScreen - [x] Verify products display correctly with proper type badges - [x] Test retry functionality when not connected - [x] Confirm both in-app and subscription products appear
1 parent a80ad86 commit 4736255

File tree

13 files changed

+436
-17
lines changed

13 files changed

+436
-17
lines changed

.github/workflows/publish.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,17 @@ jobs:
9696
tag_name: ${{ env.VERSION }}
9797
env:
9898
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
99+
100+
- name: Update version in README and openiap-versions.json
101+
run: |
102+
# Use the update script
103+
chmod +x ./scripts/update-version.sh
104+
./scripts/update-version.sh "$VERSION"
105+
106+
- name: Commit and push version updates
107+
run: |
108+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
109+
git config --local user.name "github-actions[bot]"
110+
git add README.md openiap-versions.json
111+
git diff --staged --quiet || git commit -m "chore: update version to $VERSION [skip ci]"
112+
git push origin HEAD:main

Example/src/main/java/dev/hyo/martie/MainActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ fun AppNavigation() {
4949
HomeScreen(navController)
5050
}
5151

52+
composable("all_products") {
53+
AllProductsScreen(navController)
54+
}
55+
5256
composable("purchase_flow") {
5357
PurchaseFlowScreen(navController)
5458
}
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
package dev.hyo.martie.screens
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.clickable
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.lazy.LazyColumn
7+
import androidx.compose.foundation.lazy.items
8+
import androidx.compose.foundation.shape.RoundedCornerShape
9+
import androidx.compose.material.icons.Icons
10+
import androidx.compose.material.icons.automirrored.filled.ArrowBack
11+
import androidx.compose.material.icons.filled.*
12+
import androidx.compose.material3.*
13+
import androidx.compose.runtime.*
14+
import androidx.compose.ui.Alignment
15+
import androidx.compose.ui.Modifier
16+
import androidx.compose.ui.graphics.Color
17+
import androidx.compose.ui.platform.LocalContext
18+
import androidx.compose.ui.text.font.FontWeight
19+
import androidx.compose.ui.unit.dp
20+
import androidx.navigation.NavController
21+
import dev.hyo.martie.models.AppColors
22+
import dev.hyo.martie.screens.uis.*
23+
import dev.hyo.martie.IapConstants
24+
import dev.hyo.martie.util.findActivity
25+
import dev.hyo.openiap.IapContext
26+
import dev.hyo.openiap.Product
27+
import dev.hyo.openiap.ProductAndroid
28+
import dev.hyo.openiap.ProductQueryType
29+
import dev.hyo.openiap.ProductType
30+
import dev.hyo.openiap.ProductSubscription
31+
import dev.hyo.openiap.store.OpenIapStore
32+
import dev.hyo.openiap.store.PurchaseResultStatus
33+
import kotlinx.coroutines.launch
34+
35+
@OptIn(ExperimentalMaterial3Api::class)
36+
@Composable
37+
fun AllProductsScreen(
38+
navController: NavController,
39+
storeParam: OpenIapStore? = null
40+
) {
41+
val context = LocalContext.current
42+
val activity = remember(context) { context.findActivity() }
43+
val appContext = remember(context) { context.applicationContext }
44+
val iapStore = storeParam ?: remember(appContext) { OpenIapStore(appContext) }
45+
val products by iapStore.products.collectAsState()
46+
val subscriptions by iapStore.subscriptions.collectAsState()
47+
val status by iapStore.status.collectAsState()
48+
val connectionStatus by iapStore.connectionStatus.collectAsState()
49+
50+
// Combine all products from both lists
51+
val allProducts = remember(products, subscriptions) {
52+
(products + subscriptions).filterIsInstance<ProductAndroid>()
53+
}
54+
55+
val scope = rememberCoroutineScope()
56+
57+
// Initialize and connect on first composition
58+
val startupScope = rememberCoroutineScope()
59+
DisposableEffect(Unit) {
60+
startupScope.launch {
61+
try {
62+
val connected = iapStore.initConnection()
63+
if (connected) {
64+
iapStore.setActivity(activity)
65+
// Fetch in-app products and subscriptions separately
66+
// This ensures proper type classification
67+
iapStore.fetchProducts(
68+
skus = IapConstants.INAPP_SKUS,
69+
type = ProductQueryType.InApp
70+
)
71+
iapStore.fetchProducts(
72+
skus = IapConstants.SUBS_SKUS,
73+
type = ProductQueryType.Subs
74+
)
75+
}
76+
} catch (_: Exception) { }
77+
}
78+
onDispose {
79+
// End connection when screen leaves
80+
startupScope.launch {
81+
runCatching { iapStore.endConnection() }
82+
runCatching { iapStore.clear() }
83+
}
84+
}
85+
}
86+
87+
Scaffold(
88+
topBar = {
89+
TopAppBar(
90+
title = { Text("All Products") },
91+
navigationIcon = {
92+
IconButton(onClick = { navController.popBackStack() }) {
93+
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
94+
}
95+
},
96+
colors = TopAppBarDefaults.topAppBarColors(
97+
containerColor = AppColors.cardBackground,
98+
titleContentColor = AppColors.textPrimary
99+
)
100+
)
101+
}
102+
) { paddingValues ->
103+
Column(
104+
modifier = Modifier
105+
.fillMaxSize()
106+
.padding(paddingValues)
107+
.background(AppColors.background)
108+
) {
109+
if (!connectionStatus) {
110+
Card(
111+
modifier = Modifier
112+
.fillMaxWidth()
113+
.padding(16.dp),
114+
colors = CardDefaults.cardColors(containerColor = AppColors.warning.copy(alpha = 0.1f))
115+
) {
116+
Row(
117+
modifier = Modifier.padding(16.dp),
118+
verticalAlignment = Alignment.CenterVertically
119+
) {
120+
Icon(
121+
Icons.Default.Warning,
122+
contentDescription = null,
123+
tint = AppColors.warning
124+
)
125+
Spacer(modifier = Modifier.width(12.dp))
126+
Column {
127+
Text(
128+
"Not Connected",
129+
style = MaterialTheme.typography.bodyMedium,
130+
fontWeight = FontWeight.Bold,
131+
color = AppColors.textPrimary
132+
)
133+
Text(
134+
"Billing service is not connected. Tap to retry.",
135+
style = MaterialTheme.typography.bodySmall,
136+
color = AppColors.textSecondary
137+
)
138+
}
139+
Spacer(modifier = Modifier.weight(1f))
140+
TextButton(onClick = {
141+
scope.launch {
142+
try {
143+
val connected = iapStore.initConnection()
144+
if (connected) {
145+
iapStore.setActivity(activity)
146+
// Fetch products after reconnecting - separately to ensure proper types
147+
iapStore.fetchProducts(
148+
skus = IapConstants.INAPP_SKUS,
149+
type = ProductQueryType.InApp
150+
)
151+
iapStore.fetchProducts(
152+
skus = IapConstants.SUBS_SKUS,
153+
type = ProductQueryType.Subs
154+
)
155+
}
156+
} catch (_: Exception) { }
157+
}
158+
}) {
159+
Text("Retry", color = AppColors.primary)
160+
}
161+
}
162+
}
163+
}
164+
165+
LazyColumn(
166+
modifier = Modifier.fillMaxSize(),
167+
contentPadding = PaddingValues(16.dp),
168+
verticalArrangement = Arrangement.spacedBy(12.dp)
169+
) {
170+
// Display all products in one list
171+
if (allProducts.isNotEmpty()) {
172+
items(allProducts) { product ->
173+
Card(
174+
modifier = Modifier.fillMaxWidth(),
175+
shape = RoundedCornerShape(12.dp),
176+
colors = CardDefaults.cardColors(containerColor = AppColors.cardBackground),
177+
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
178+
) {
179+
Column(modifier = Modifier.padding(16.dp)) {
180+
Row(
181+
modifier = Modifier.fillMaxWidth(),
182+
horizontalArrangement = Arrangement.SpaceBetween,
183+
verticalAlignment = Alignment.Top
184+
) {
185+
Column(
186+
modifier = Modifier.weight(1f),
187+
verticalArrangement = Arrangement.spacedBy(4.dp)
188+
) {
189+
Text(
190+
product.title,
191+
style = MaterialTheme.typography.titleMedium,
192+
fontWeight = FontWeight.Bold,
193+
color = AppColors.textPrimary
194+
)
195+
product.description?.let { desc ->
196+
Text(
197+
desc,
198+
style = MaterialTheme.typography.bodySmall,
199+
color = AppColors.textSecondary
200+
)
201+
}
202+
}
203+
// Product type badge
204+
Surface(
205+
shape = RoundedCornerShape(6.dp),
206+
color = when (product.type) {
207+
ProductType.Subs -> AppColors.primary.copy(alpha = 0.1f)
208+
else -> AppColors.success.copy(alpha = 0.1f)
209+
},
210+
modifier = Modifier.padding(start = 8.dp)
211+
) {
212+
Text(
213+
text = when (product.type) {
214+
ProductType.Subs -> "subs"
215+
else -> "in-app"
216+
},
217+
style = MaterialTheme.typography.labelSmall,
218+
color = when (product.type) {
219+
ProductType.Subs -> AppColors.primary
220+
else -> AppColors.success
221+
},
222+
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
223+
)
224+
}
225+
}
226+
Spacer(modifier = Modifier.height(8.dp))
227+
Row(
228+
modifier = Modifier.fillMaxWidth(),
229+
horizontalArrangement = Arrangement.SpaceBetween,
230+
verticalAlignment = Alignment.CenterVertically
231+
) {
232+
Text(
233+
product.price?.toString() ?: "--",
234+
style = MaterialTheme.typography.titleLarge,
235+
color = AppColors.primary,
236+
fontWeight = FontWeight.Bold
237+
)
238+
Row(
239+
horizontalArrangement = Arrangement.spacedBy(4.dp),
240+
verticalAlignment = Alignment.CenterVertically
241+
) {
242+
Text(
243+
"SKU: ${product.id}",
244+
style = MaterialTheme.typography.labelSmall,
245+
color = AppColors.textSecondary
246+
)
247+
}
248+
}
249+
}
250+
}
251+
}
252+
}
253+
254+
// Empty state when no products and not loading
255+
if (!status.isLoading && allProducts.isEmpty() && connectionStatus) {
256+
item {
257+
EmptyStateCard(
258+
message = "No products available",
259+
icon = Icons.Default.ShoppingBag
260+
)
261+
}
262+
}
263+
264+
// Loading indicator
265+
if (status.isLoading) {
266+
item {
267+
LoadingCard()
268+
}
269+
}
270+
271+
// Status message
272+
status.lastPurchaseResult?.let { message ->
273+
item {
274+
PurchaseResultCard(
275+
message = message.toString(),
276+
status = PurchaseResultStatus.Success,
277+
onDismiss = { /* TODO */ }
278+
)
279+
}
280+
}
281+
282+
// Error message
283+
status.lastError?.let { err ->
284+
item {
285+
Card(
286+
modifier = Modifier.fillMaxWidth(),
287+
colors = CardDefaults.cardColors(containerColor = AppColors.danger.copy(alpha = 0.1f))
288+
) {
289+
Row(
290+
modifier = Modifier.padding(16.dp),
291+
verticalAlignment = Alignment.CenterVertically
292+
) {
293+
Icon(
294+
Icons.Default.Warning,
295+
contentDescription = null,
296+
tint = AppColors.danger
297+
)
298+
Spacer(modifier = Modifier.width(12.dp))
299+
Text(
300+
err.message,
301+
style = MaterialTheme.typography.bodyMedium,
302+
color = AppColors.danger
303+
)
304+
}
305+
}
306+
}
307+
}
308+
}
309+
}
310+
}
311+
}

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

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

33
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.clickable
45
import androidx.compose.foundation.layout.*
56
import androidx.compose.foundation.lazy.grid.GridCells
67
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
@@ -38,7 +39,8 @@ fun HomeScreen(navController: NavController) {
3839
Card(
3940
modifier = Modifier
4041
.fillMaxWidth()
41-
.padding(horizontal = 16.dp),
42+
.padding(horizontal = 16.dp)
43+
.clickable { navController.navigate("all_products") },
4244
shape = RoundedCornerShape(12.dp),
4345
colors = CardDefaults.cardColors(containerColor = AppColors.cardBackground),
4446
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ Add to your module's `build.gradle.kts`:
4848

4949
```kotlin
5050
dependencies {
51-
implementation("io.github.hyochan.openiap:openiap-google:1.2.6")
51+
implementation("io.github.hyochan.openiap:openiap-google:1.2.7")
5252
}
5353
```
5454

5555
Or `build.gradle`:
5656

5757
```groovy
5858
dependencies {
59-
implementation 'io.github.hyochan.openiap:openiap-google:1.2.6'
59+
implementation 'io.github.hyochan.openiap:openiap-google:1.2.7'
6060
}
6161
```
6262

build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ plugins {
66
id("com.vanniktech.maven.publish") version "0.29.0" apply false
77
}
88

9+
import java.io.File
10+
11+
val versionsFile = File(rootDir, "openiap-versions.json")
12+
val jsonText = versionsFile.readText()
13+
val googleVersion = jsonText.substringAfter("\"google\": \"").substringBefore("\"")
14+
val gqlVersion = jsonText.substringAfter("\"gql\": \"").substringBefore("\"")
15+
16+
extra["OPENIAP_VERSION"] = googleVersion
17+
extra["GQL_VERSION"] = gqlVersion
18+
919
// Configure Sonatype (OSSRH) publishing at the root
1020
// Credentials are sourced from env or gradle.properties (OSSRH_USERNAME/OSSRH_PASSWORD)
1121
// Maven Central publishing is configured per-module via Vanniktech plugin.

0 commit comments

Comments
 (0)