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+ }
0 commit comments