Skip to content
This repository was archived by the owner on Oct 17, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Example/src/main/java/dev/hyo/martie/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ object IapConstants {
)

val SUBS_SKUS = listOf(
"dev.hyo.martie.premium",
"dev.hyo.martie.premium_year"
"dev.hyo.martie.premium", // Main subscription with multiple offers
"dev.hyo.martie.premium_year" // Separate yearly subscription product
)

// Base plan IDs for dev.hyo.martie.premium subscription
const val PREMIUM_MONTHLY_BASE_PLAN = "premium" // Monthly base plan
const val PREMIUM_YEARLY_BASE_PLAN = "premium-year" // Yearly base plan
}

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import dev.hyo.openiap.Product
import dev.hyo.openiap.ProductAndroid
import dev.hyo.openiap.ProductQueryType
import dev.hyo.openiap.ProductType
import dev.hyo.openiap.ProductRequest
import dev.hyo.openiap.ProductSubscription
import dev.hyo.openiap.store.OpenIapStore
import dev.hyo.openiap.store.PurchaseResultStatus
Expand Down Expand Up @@ -64,10 +65,11 @@ fun AllProductsScreen(
iapStore.setActivity(activity)
// Fetch all products at once using ProductQueryType.All
// This fetches both in-app and subscription products in a single call
iapStore.fetchProducts(
val request = ProductRequest(
skus = IapConstants.INAPP_SKUS + IapConstants.SUBS_SKUS,
type = ProductQueryType.All
)
iapStore.fetchProducts(request)
}
} catch (_: Exception) { }
}
Expand Down Expand Up @@ -140,10 +142,11 @@ fun AllProductsScreen(
if (connected) {
iapStore.setActivity(activity)
// Fetch all products after reconnecting using ProductQueryType.All
iapStore.fetchProducts(
val request = ProductRequest(
skus = IapConstants.INAPP_SKUS + IapConstants.SUBS_SKUS,
type = ProductQueryType.All
)
iapStore.fetchProducts(request)
}
} catch (_: Exception) { }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import dev.hyo.martie.models.AppColors
import dev.hyo.martie.screens.uis.*
import dev.hyo.martie.util.PREMIUM_SUBSCRIPTION_PRODUCT_ID
import dev.hyo.openiap.IapContext
import dev.hyo.openiap.PurchaseAndroid
import dev.hyo.openiap.PurchaseState
Expand Down Expand Up @@ -53,7 +54,7 @@ fun AvailablePurchasesScreen(
try {
val connected = iapStore.initConnection()
if (connected) {
iapStore.getAvailablePurchases()
iapStore.getAvailablePurchases(null)
}
} catch (_: Exception) { }
}
Expand All @@ -77,7 +78,7 @@ fun AvailablePurchasesScreen(
onClick = {
scope.launch {
try {
val restored = iapStore.restorePurchases()
val restored = iapStore.getAvailablePurchases(null)
iapStore.postStatusMessage(
message = "Restored ${restored.size} purchases",
status = PurchaseResultStatus.Success
Expand Down Expand Up @@ -185,14 +186,14 @@ fun AvailablePurchasesScreen(
}
val nonConsumables = androidPurchases.filter {
!it.isAutoRenewing &&
it.productId != "dev.hyo.martie.premium" &&
it.productId != PREMIUM_SUBSCRIPTION_PRODUCT_ID &&
!(
it.productId.contains("consumable", ignoreCase = true) ||
it.productId.contains("bulb", ignoreCase = true)
)
}
val subscriptions = androidPurchases.filter {
it.isAutoRenewing || it.productId == "dev.hyo.martie.premium"
it.isAutoRenewing || it.productId == PREMIUM_SUBSCRIPTION_PRODUCT_ID
}

// Check for unfinished transactions (purchases that need acknowledgment/consumption)
Expand All @@ -217,21 +218,24 @@ fun AvailablePurchasesScreen(
onFinish = { isConsumable ->
scope.launch {
try {
val ok = iapStore.finishTransaction(purchase, isConsumable)
if (ok) {
iapStore.postStatusMessage(
message = "Transaction finished successfully",
status = PurchaseResultStatus.Success,
productId = purchase.productId
)
iapStore.getAvailablePurchases()
} else {
iapStore.postStatusMessage(
message = "Failed to finish transaction",
status = PurchaseResultStatus.Error,
productId = purchase.productId
)
}
val purchaseInput = dev.hyo.openiap.PurchaseInput(
id = purchase.id,
ids = purchase.ids,
isAutoRenewing = purchase.isAutoRenewing,
platform = purchase.platform,
productId = purchase.productId,
purchaseState = purchase.purchaseState,
purchaseToken = purchase.purchaseToken,
quantity = purchase.quantity,
transactionDate = purchase.transactionDate
)
iapStore.finishTransaction(purchaseInput, isConsumable)
iapStore.postStatusMessage(
message = "Transaction finished successfully",
status = PurchaseResultStatus.Success,
productId = purchase.productId
)
iapStore.getAvailablePurchases(null)
} catch (e: Exception) {
iapStore.postStatusMessage(
message = e.message ?: "Failed to finish transaction",
Expand Down Expand Up @@ -399,7 +403,7 @@ fun AvailablePurchasesScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedButton(
onClick = { scope.launch { iapStore.getAvailablePurchases() } },
onClick = { scope.launch { iapStore.getAvailablePurchases(null) } },
modifier = Modifier.weight(1f),
enabled = !status.isLoading
) {
Expand All @@ -412,7 +416,7 @@ fun AvailablePurchasesScreen(
onClick = {
scope.launch {
try {
val restored = iapStore.restorePurchases()
val restored = iapStore.getAvailablePurchases(null)
iapStore.postStatusMessage(
message = "Restored ${restored.size} purchases",
status = PurchaseResultStatus.Success
Expand Down
120 changes: 77 additions & 43 deletions Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,17 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import dev.hyo.openiap.ProductAndroid
import dev.hyo.openiap.ProductQueryType
import dev.hyo.openiap.ProductRequest
import dev.hyo.openiap.ProductType
import dev.hyo.openiap.Purchase
import dev.hyo.openiap.PurchaseAndroid
import dev.hyo.openiap.PurchaseInput
import dev.hyo.openiap.RequestPurchaseProps
import dev.hyo.openiap.RequestPurchaseAndroidProps
import dev.hyo.openiap.RequestPurchasePropsByPlatforms
import dev.hyo.openiap.RequestSubscriptionAndroidProps
import dev.hyo.openiap.RequestSubscriptionPropsByPlatforms
import dev.hyo.openiap.utils.toPurchaseInput
import dev.hyo.martie.util.findActivity

@OptIn(ExperimentalMaterial3Api::class)
Expand Down Expand Up @@ -74,11 +82,12 @@ fun PurchaseFlowScreen(
val connected = iapStore.initConnection()
if (connected) {
iapStore.setActivity(activity)
iapStore.fetchProducts(
val request = ProductRequest(
skus = IapConstants.INAPP_SKUS,
type = ProductQueryType.InApp
)
iapStore.getAvailablePurchases()
iapStore.fetchProducts(request)
iapStore.getAvailablePurchases(null)
}
} catch (_: Exception) { }
}
Expand Down Expand Up @@ -107,10 +116,11 @@ fun PurchaseFlowScreen(
scope.launch {
try {
iapStore.setActivity(activity)
iapStore.fetchProducts(
val request = ProductRequest(
skus = IapConstants.INAPP_SKUS,
type = ProductQueryType.InApp
)
iapStore.fetchProducts(request)
} catch (_: Exception) { }
}
},
Expand Down Expand Up @@ -238,13 +248,32 @@ fun PurchaseFlowScreen(
isPurchasing = status.isPurchasing(androidProduct.id),
onPurchase = {
scope.launch {
val reqType = if (androidProduct.type == ProductType.Subs)
ProductQueryType.Subs else ProductQueryType.InApp
iapStore.setActivity(activity)
iapStore.requestPurchase(
skus = listOf(androidProduct.id),
type = reqType
)
if (androidProduct.type == ProductType.Subs) {
val props = RequestPurchaseProps(
request = RequestPurchaseProps.Request.Subscription(
RequestSubscriptionPropsByPlatforms(
android = RequestSubscriptionAndroidProps(
skus = listOf(androidProduct.id)
)
)
),
type = ProductQueryType.Subs
)
iapStore.requestPurchase(props)
} else {
val props = RequestPurchaseProps(
request = RequestPurchaseProps.Request.Purchase(
RequestPurchasePropsByPlatforms(
android = RequestPurchaseAndroidProps(
skus = listOf(androidProduct.id)
)
)
),
type = ProductQueryType.InApp
)
iapStore.requestPurchase(props)
}
}
},
onClick = {
Expand All @@ -263,23 +292,7 @@ fun PurchaseFlowScreen(
)
}
}

// Active Purchases Section
if (androidPurchases.isNotEmpty()) {
item {
SectionHeaderView(title = "Active Purchases")
}

items(androidPurchases) { androidPurchase ->
ActivePurchaseCard(
purchase = androidPurchase,
onClick = {
selectedPurchase = androidPurchase
}
)
}
}


// Instructions Card
item {
InstructionCard()
Expand All @@ -297,7 +310,7 @@ fun PurchaseFlowScreen(
onClick = {
scope.launch {
try {
val restored = iapStore.restorePurchases()
val restored = iapStore.getAvailablePurchases(null)
iapStore.postStatusMessage(
message = "Restored ${restored.size} purchases",
status = PurchaseResultStatus.Success
Expand All @@ -322,10 +335,11 @@ fun PurchaseFlowScreen(
onClick = {
scope.launch {
try {
iapStore.fetchProducts(
val request = ProductRequest(
skus = IapConstants.INAPP_SKUS,
type = ProductQueryType.InApp
)
iapStore.fetchProducts(request)
} catch (_: Exception) { }
}
},
Expand Down Expand Up @@ -380,21 +394,22 @@ fun PurchaseFlowScreen(
}

// 4) Finish transaction
val ok = iapStore.finishTransaction(purchase, isConsumable)
if (!ok) {
iapStore.postStatusMessage(
message = "finishTransaction failed",
status = PurchaseResultStatus.Error,
productId = purchase.productId
)
} else {
iapStore.loadPurchases()
val purchaseInput = purchase.toPurchaseInput()
try {
iapStore.finishTransaction(purchaseInput, isConsumable)
iapStore.getAvailablePurchases(null) // Reload purchases after finishing
iapStore.postStatusMessage(
message = "Purchase finished successfully",
status = PurchaseResultStatus.Success,
productId = purchase.productId
)
selectedProduct = null
} catch (e: Exception) {
iapStore.postStatusMessage(
message = "finishTransaction failed: ${e.message}",
status = PurchaseResultStatus.Error,
productId = purchase.productId
)
}
} catch (e: Exception) {
iapStore.postStatusMessage(
Expand All @@ -412,13 +427,32 @@ fun PurchaseFlowScreen(
onDismiss = { selectedProduct = null },
onPurchase = {
uiScope.launch {
val reqType = if (product.type == ProductType.Subs)
ProductQueryType.Subs else ProductQueryType.InApp
iapStore.setActivity(activity)
iapStore.requestPurchase(
skus = listOf(product.id),
type = reqType
)
if (product.type == ProductType.Subs) {
val props = RequestPurchaseProps(
request = RequestPurchaseProps.Request.Subscription(
RequestSubscriptionPropsByPlatforms(
android = RequestSubscriptionAndroidProps(
skus = listOf(product.id)
)
)
),
type = ProductQueryType.Subs
)
iapStore.requestPurchase(props)
} else {
val props = RequestPurchaseProps(
request = RequestPurchaseProps.Request.Purchase(
RequestPurchasePropsByPlatforms(
android = RequestPurchaseAndroidProps(
skus = listOf(product.id)
)
)
),
type = ProductQueryType.InApp
)
iapStore.requestPurchase(props)
}
}
},
isPurchasing = status.isPurchasing(product.id)
Expand Down
Loading
Loading