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
7 changes: 7 additions & 0 deletions CONVENTION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Project Conventions

## Naming Conventions

### Enum Values
- Enum values in this codebase must use **kebab-case** (e.g., `non-consumable`, `in-app`, `user-cancelled`)
- This matches the convention used in the auto-generated Types.kt from GraphQL schemas
- Do not use snake_case (e.g., `non_consumable`) or camelCase for enum raw values

## Generated GraphQL/Kotlin Models

- `openiap/src/main/Types.kt` is auto-generated. Regenerate it with `./scripts/generate-types.sh` after changing any GraphQL schema files.
Expand Down
6 changes: 4 additions & 2 deletions Example/src/main/java/dev/hyo/martie/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ object IapConstants {
// App-defined SKU lists
val INAPP_SKUS = listOf(
"dev.hyo.martie.10bulbs",
"dev.hyo.martie.30bulbs"
"dev.hyo.martie.30bulbs",
"dev.hyo.martie.certified" // Non-consumable
)

val SUBS_SKUS = listOf(
"dev.hyo.martie.premium"
"dev.hyo.martie.premium",
"dev.hyo.martie.premium_year"
)
}

22 changes: 7 additions & 15 deletions Example/src/main/java/dev/hyo/martie/screens/AllProductsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,11 @@ fun AllProductsScreen(
val connected = iapStore.initConnection()
if (connected) {
iapStore.setActivity(activity)
// Fetch in-app products and subscriptions separately
// This ensures proper type classification
// Fetch all products at once using ProductQueryType.All
// This fetches both in-app and subscription products in a single call
iapStore.fetchProducts(
skus = IapConstants.INAPP_SKUS,
type = ProductQueryType.InApp
)
iapStore.fetchProducts(
skus = IapConstants.SUBS_SKUS,
type = ProductQueryType.Subs
skus = IapConstants.INAPP_SKUS + IapConstants.SUBS_SKUS,
type = ProductQueryType.All
)
}
} catch (_: Exception) { }
Expand Down Expand Up @@ -143,14 +139,10 @@ fun AllProductsScreen(
val connected = iapStore.initConnection()
if (connected) {
iapStore.setActivity(activity)
// Fetch products after reconnecting - separately to ensure proper types
iapStore.fetchProducts(
skus = IapConstants.INAPP_SKUS,
type = ProductQueryType.InApp
)
// Fetch all products after reconnecting using ProductQueryType.All
iapStore.fetchProducts(
skus = IapConstants.SUBS_SKUS,
type = ProductQueryType.Subs
skus = IapConstants.INAPP_SKUS + IapConstants.SUBS_SKUS,
type = ProductQueryType.All
)
}
} catch (_: Exception) { }
Expand Down
64 changes: 47 additions & 17 deletions openiap/src/main/java/dev/hyo/openiap/OpenIapModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -115,26 +115,56 @@ class OpenIapModule(private val context: Context) : PurchasesUpdatedListener {
if (params.skus.isEmpty() && params.type != ProductQueryType.All) throw OpenIapError.EmptySkuList

val queryType = params.type ?: ProductQueryType.All
val includeInApp = queryType == ProductQueryType.InApp || queryType == ProductQueryType.All
val includeSubs = queryType == ProductQueryType.Subs || queryType == ProductQueryType.All

val inAppProducts = if (includeInApp) {
queryProductDetails(client, productManager, params.skus, BillingClient.ProductType.INAPP)
.map { it.toInAppProduct() }
} else emptyList()

val subscriptionProducts = if (includeSubs) {
queryProductDetails(client, productManager, params.skus, BillingClient.ProductType.SUBS)
.map { it.toSubscriptionProduct() }
} else emptyList()

when (queryType) {
ProductQueryType.InApp -> FetchProductsResultProducts(inAppProducts)
ProductQueryType.Subs -> FetchProductsResultSubscriptions(subscriptionProducts)
ProductQueryType.InApp -> {
val inAppProducts = queryProductDetails(client, productManager, params.skus, BillingClient.ProductType.INAPP)
.map { it.toInAppProduct() }
FetchProductsResultProducts(inAppProducts)
}
ProductQueryType.Subs -> {
val subscriptionProducts = queryProductDetails(client, productManager, params.skus, BillingClient.ProductType.SUBS)
.map { it.toSubscriptionProduct() }
FetchProductsResultSubscriptions(subscriptionProducts)
}
ProductQueryType.All -> {
// For All type, combine products and return as Products result
val allProducts = inAppProducts + subscriptionProducts.filterIsInstance<ProductSubscriptionAndroid>().map { it.toProduct() }
FetchProductsResultProducts(allProducts)
// Query both types and combine results
val allProducts = mutableListOf<Product>()
val processedIds = mutableSetOf<String>()

// First, get all INAPP products
val inAppDetails = runCatching {
queryProductDetails(client, productManager, params.skus, BillingClient.ProductType.INAPP)
}.getOrDefault(emptyList())

inAppDetails.forEach { detail ->
val product = detail.toInAppProduct()
allProducts.add(product)
processedIds.add(detail.productId)
}

// Then, get subscription products (only add if not already processed as INAPP)
val subsDetails = runCatching {
queryProductDetails(client, productManager, params.skus, BillingClient.ProductType.SUBS)
}.getOrDefault(emptyList())

subsDetails.forEach { detail ->
if (detail.productId !in processedIds) {
// Keep subscription as ProductSubscription, but convert to Product for return
val subProduct = detail.toSubscriptionProduct()
allProducts.add(subProduct.toProduct())
}
}

// Return products in the order they were requested if SKUs provided
val orderedProducts = if (params.skus.isNotEmpty()) {
val productMap = allProducts.associateBy { it.id }
params.skus.mapNotNull { productMap[it] }
} else {
allProducts
}

FetchProductsResultProducts(orderedProducts)
}
}
}
Expand Down
Loading