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

Commit bb8ddbd

Browse files
authored
Adopt openiap-gql typegen on Android (#9)
## Summary - replace handwritten models with openiap-gql generated Types.kt - rewire OpenIapModule, store, and sample screens to the new handlers - fail CI when Types.kt regeneration alters the tree
1 parent 0e39464 commit bb8ddbd

35 files changed

+3446
-1832
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ jobs:
4949
"build-tools;34.0.0"
5050
yes | sdkmanager --licenses
5151
52+
- name: Verify generated Types.kt is up to date
53+
run: |
54+
./scripts/generate-types.sh
55+
git diff --exit-code openiap/src/main/java/dev/hyo/openiap/Types.kt
56+
5257
- name: Build library and example
5358
run: ./gradlew --stacktrace --no-daemon :openiap:build :Example:assembleDebug
54-

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
"npm.autoDetect": "off",
4343
"cSpell.words": [
4444
"billingclient",
45-
"openiap"
45+
"gson",
46+
"openiap",
47+
"skus"
4648
]
4749
}

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CLAUDE.md

CLAUDE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Agent Primer
2+
3+
Welcome! This repository hosts the Android implementation of OpenIAP.
4+
5+
## Project Layout
6+
7+
- `openiap/`: Android library sources.
8+
- `Example/`: sample application consuming the library.
9+
- `scripts/`: automation (code generation, tooling).
10+
- `CONVENTION.md`: authoritative engineering conventions for this repo.
11+
12+
## How To Work Here
13+
14+
1. Start every session by reading `CONVENTION.md`. It documents critical rules such as the prohibition on editing generated files (`openiap/src/main/Types.kt`) and where to place shared helper code (`openiap/src/main/java/dev/hyo/openiap/utils/…`).
15+
2. Treat generated sources as read-only. If a change requires updating them, run `./scripts/generate-types.sh` instead of hand editing.
16+
3. Put all reusable Kotlin helpers (e.g., safe map accessors) into the `utils` package so they can be used without modifying generated output.
17+
4. After code generation or dependency changes, compile with `./gradlew :openiap:compileDebugKotlin` (or the appropriate target) to verify the build stays green.
18+
19+
Refer back to this document and `CONVENTION.md` whenever you are unsure about workflow expectations.

CONVENTION.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Project Conventions
2+
3+
## Generated GraphQL/Kotlin Models
4+
5+
- `openiap/src/main/Types.kt` is auto-generated. Regenerate it with `./scripts/generate-types.sh` after changing any GraphQL schema files.
6+
- Never edit `Types.kt` manually. Regeneration guarantees consistency across platforms and avoids merge conflicts.
7+
- When additional parsing or conversion helpers are needed for GraphQL payloads, place them in a utility file (for example `openiap/src/main/java/dev/hyo/openiap/utils/JsonUtils.kt`). Keep all custom helpers outside of generated sources and have the hand-written code call into them.
8+
9+
## Helper Utilities
10+
11+
- Shared helper extensions such as safe `Map<String, *>` lookups must live in utility sources (`utils/*.kt`) so they can be reused without modifying generated files.
12+
- Utility files should include succinct KDoc explaining their intent and reference the convention above when interacting with generated code.
13+
14+
## Android Module API Handlers
15+
16+
- The Android `OpenIapModule` exposes every GraphQL operation through the typealias handlers defined in `Types.kt` (e.g. `MutationInitConnectionHandler`, `QueryGetAvailablePurchasesHandler`, etc.).
17+
- These handlers are declared as properties (for example `val initConnection = ...`) inside `OpenIapModule`; they encapsulate all coroutine work (`withContext`, `suspendCancellableCoroutine`, etc.) and return the types required by the GraphQL schema (e.g. `RequestPurchaseResult`).
18+
- `OpenIapStore` and other consumers must call the module through these handler properties rather than direct suspend functions, unpacking any wrapper results (such as `RequestPurchaseResultPurchases`) as needed.
19+
- Keep helper wiring inside `OpenIapModule`—avoid reintroducing extension builders like `createQueryHandlers`; the module itself owns `queryHandlers`, `mutationHandlers`, and `subscriptionHandlers` values so wiring stays localized and in sync with the typealiases.
20+
21+
## Regeneration Checklist
22+
23+
- Run `./scripts/generate-types.sh` whenever GraphQL schema definitions change.
24+
- After regenerating, run the relevant Gradle targets (e.g. `./gradlew :openiap:compileDebugKotlin`) to ensure the generated output compiles together with existing handwritten code.

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import androidx.navigation.NavController
2121
import dev.hyo.martie.models.AppColors
2222
import dev.hyo.martie.screens.uis.*
2323
import dev.hyo.openiap.IapContext
24+
import dev.hyo.openiap.PurchaseAndroid
25+
import dev.hyo.openiap.PurchaseState
2426
import dev.hyo.openiap.store.OpenIapStore
25-
import dev.hyo.openiap.models.*
2627
import dev.hyo.openiap.store.PurchaseResultStatus
2728
import kotlinx.coroutines.launch
2829

@@ -39,9 +40,11 @@ fun AvailablePurchasesScreen(
3940
val status by iapStore.status.collectAsState()
4041
val connectionStatus by iapStore.connectionStatus.collectAsState()
4142
val statusMessage = status.lastPurchaseResult
42-
43+
44+
val androidPurchases = remember(purchases) { purchases.filterIsInstance<PurchaseAndroid>() }
45+
4346
// Modal state
44-
var selectedPurchase by remember { mutableStateOf<OpenIapPurchase?>(null) }
47+
var selectedPurchase by remember { mutableStateOf<PurchaseAndroid?>(null) }
4548

4649
// Initialize and connect on first composition (spec-aligned names)
4750
val startupScope = rememberCoroutineScope()
@@ -174,26 +177,26 @@ fun AvailablePurchasesScreen(
174177
}
175178

176179
// Group purchases by type
177-
val consumables = purchases.filter {
180+
val consumables = androidPurchases.filter {
178181
!it.isAutoRenewing && (
179182
it.productId.contains("consumable", ignoreCase = true) ||
180183
it.productId.contains("bulb", ignoreCase = true)
181184
)
182185
}
183-
val nonConsumables = purchases.filter {
186+
val nonConsumables = androidPurchases.filter {
184187
!it.isAutoRenewing &&
185188
it.productId != "dev.hyo.martie.premium" &&
186189
!(
187190
it.productId.contains("consumable", ignoreCase = true) ||
188191
it.productId.contains("bulb", ignoreCase = true)
189192
)
190193
}
191-
val subscriptions = purchases.filter {
194+
val subscriptions = androidPurchases.filter {
192195
it.isAutoRenewing || it.productId == "dev.hyo.martie.premium"
193196
}
194197

195198
// Check for unfinished transactions (purchases that need acknowledgment/consumption)
196-
val unfinishedPurchases = purchases.filter { purchase ->
199+
val unfinishedPurchases = androidPurchases.filter { purchase ->
197200
// TODO: In real implementation, check if purchase needs acknowledgment/consumption
198201
// This would typically check: purchase.purchaseState == PurchaseState.Purchased && !purchase.isAcknowledged
199202
// For demo purposes, let's assume some consumable purchases might need finishing
@@ -326,7 +329,7 @@ fun AvailablePurchasesScreen(
326329
}
327330

328331
// Empty State
329-
if (purchases.isEmpty() && !status.isLoading) {
332+
if (androidPurchases.isEmpty() && !status.isLoading) {
330333
item {
331334
EmptyStateCard(
332335
message = "No purchases found. Try restoring purchases from your Google account.",
@@ -336,7 +339,7 @@ fun AvailablePurchasesScreen(
336339
}
337340

338341
// Statistics Card
339-
if (purchases.isNotEmpty()) {
342+
if (androidPurchases.isNotEmpty()) {
340343
item {
341344
Card(
342345
modifier = Modifier
@@ -362,7 +365,7 @@ fun AvailablePurchasesScreen(
362365
horizontalArrangement = Arrangement.SpaceAround
363366
) {
364367
StatisticItem(
365-
count = purchases.size,
368+
count = androidPurchases.size,
366369
label = "Total",
367370
color = AppColors.primary
368371
)
@@ -451,7 +454,7 @@ enum class PurchaseType {
451454

452455
@Composable
453456
fun PurchaseItemCard(
454-
purchase: OpenIapPurchase,
457+
purchase: PurchaseAndroid,
455458
type: PurchaseType,
456459
onClick: () -> Unit
457460
) {
@@ -509,7 +512,7 @@ fun PurchaseItemCard(
509512
)
510513

511514
Text(
512-
"Purchased: ${java.text.SimpleDateFormat("MMM dd, yyyy", java.util.Locale.getDefault()).format(java.util.Date(purchase.transactionDate))}",
515+
"Purchased: ${java.text.SimpleDateFormat("MMM dd, yyyy", java.util.Locale.getDefault()).format(java.util.Date(purchase.transactionDate.toLong()))}",
513516
style = MaterialTheme.typography.bodySmall,
514517
color = AppColors.textSecondary
515518
)
@@ -567,7 +570,7 @@ fun StatisticItem(
567570

568571
@Composable
569572
fun UnfinishedTransactionCard(
570-
purchase: OpenIapPurchase,
573+
purchase: PurchaseAndroid,
571574
onFinish: (Boolean) -> Unit,
572575
onClick: () -> Unit
573576
) {
@@ -617,7 +620,7 @@ fun UnfinishedTransactionCard(
617620
)
618621

619622
Text(
620-
"Date: ${java.text.SimpleDateFormat("MMM dd, yyyy", java.util.Locale.getDefault()).format(java.util.Date(purchase.transactionDate))}",
623+
"Date: ${java.text.SimpleDateFormat("MMM dd, yyyy", java.util.Locale.getDefault()).format(java.util.Date(purchase.transactionDate.toLong()))}",
621624
style = MaterialTheme.typography.bodySmall,
622625
color = AppColors.textSecondary
623626
)

0 commit comments

Comments
 (0)