Skip to content
This repository was archived by the owner on Oct 17, 2025. It is now read-only.
Closed
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
30 changes: 30 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@
],
"console": "integratedTerminal"
},
{
"name": "🧱 Android: Assemble Example (Horizon/Auto)",
"type": "node",
"request": "launch",
"runtimeExecutable": "bash",
"runtimeArgs": [
"-lc",
"./gradlew :Example:assembleDebug -PEXAMPLE_OPENIAP_STORE=auto"
],
"console": "integratedTerminal"
},
{
"name": "▶️ Android: Start Example Activity",
"type": "node",
Expand Down Expand Up @@ -66,6 +77,25 @@
"./gradlew :openiap:test --no-daemon"
],
"console": "integratedTerminal"
},
{
"name": "🎯 Android: Run Example (Horizon Force)",
"type": "node",
"request": "launch",
"runtimeExecutable": "bash",
"runtimeArgs": [
"-lc",
"./gradlew :Example:installDebug -PEXAMPLE_OPENIAP_STORE=horizon -PEXAMPLE_HORIZON_APP_ID='${input:horizonAppId}' && adb shell am start -n dev.hyo.martie/.MainActivity"
],
"console": "integratedTerminal"
}
],
"inputs": [
{
"id": "horizonAppId",
"type": "promptString",
"description": "Enter Horizon APP_ID",
"default": ""
}
]
}
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ cd openiap-google
adb shell am start -n dev.hyo.martie/.MainActivity
```

### Horizon Quickstart (Local)

The core library ships Play-only by default but supports Horizon if a provider is on the classpath.

- Provider selection is done via `OpenIapStore(context, store?, appId?)` or BuildConfig flags.
- Run Example targeting Horizon/auto:
- VS Code: use launch config "Android: Run Example (Horizon/Auto)"
- CLI: `./gradlew :Example:installDebug -PEXAMPLE_OPENIAP_STORE=auto`
- CLI (force): `./gradlew :Example:installDebug -PEXAMPLE_OPENIAP_STORE=horizon -PEXAMPLE_HORIZON_APP_ID=YOUR_APP_ID`
- VS Code (force): use launch config "Android: Run Example (Horizon Force)" — it will prompt for `HORIZON_APP_ID` via input box and pass `-PEXAMPLE_HORIZON_APP_ID` to Gradle.
- Notes:
- Use a Quest device with Meta services; emulators are not supported.
- If the provider is missing, the factory falls back to Play when using `auto`.

## Code Style

- Follow the official Kotlin Coding Conventions
Expand Down
12 changes: 12 additions & 0 deletions Example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ android {

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true

// Optional store override for example app: play | horizon | auto
val store = (project.findProperty("EXAMPLE_OPENIAP_STORE") as String?) ?: "play"
buildConfigField("String", "OPENIAP_STORE", "\"${store}\"")

// Optional Horizon app id (provider-specific)
// Prefer EXAMPLE_HORIZON_APP_ID; fallback to legacy EXAMPLE_OPENIAP_APP_ID if provided
val appId = (project.findProperty("EXAMPLE_HORIZON_APP_ID") as String?)
?: (project.findProperty("EXAMPLE_OPENIAP_APP_ID") as String?)
?: ""
buildConfigField("String", "HORIZON_APP_ID", "\"${appId}\"")
}

buildTypes {
Expand Down Expand Up @@ -44,6 +55,7 @@ android {

buildFeatures {
compose = true
buildConfig = true
}

packaging {
Expand Down
25 changes: 19 additions & 6 deletions Example/src/main/java/dev/hyo/martie/Constants.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
package dev.hyo.martie

object IapConstants {
// App-defined SKU lists
val INAPP_SKUS = listOf(
private fun isHorizon(): Boolean =
dev.hyo.martie.BuildConfig.OPENIAP_STORE.equals("horizon", ignoreCase = true)

// Define your Horizon product IDs here (placeholders)
private val HORIZON_INAPP = listOf(
"dev.hyo.martie.10bulbs",
"dev.hyo.martie.30bulbs"
"dev.hyo.martie.30bulbs",
)
private val HORIZON_SUBS = listOf(
"dev.hyo.martie.premium",
)

val SUBS_SKUS = listOf(
"dev.hyo.martie.premium"
// Google Play product IDs (existing)
private val PLAY_INAPP = listOf(
"dev.hyo.martie.10bulbs",
"dev.hyo.martie.30bulbs",
)
private val PLAY_SUBS = listOf(
"dev.hyo.martie.premium",
)
}

fun inappSkus(): List<String> = if (isHorizon()) HORIZON_INAPP else PLAY_INAPP
fun subsSkus(): List<String> = if (isHorizon()) HORIZON_SUBS else PLAY_SUBS
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,15 @@ fun AvailablePurchasesScreen(
storeParam: OpenIapStore? = null
) {
val context = LocalContext.current
val appContext = context.applicationContext
val iapStore = storeParam ?: (IapContext.LocalOpenIapStore.current
?: IapContext.rememberOpenIapStore())
?: remember(appContext) {
val storeKey = dev.hyo.martie.BuildConfig.OPENIAP_STORE
val appId = dev.hyo.martie.BuildConfig.HORIZON_APP_ID
android.util.Log.i("OpenIapFactory", "example-create storeKey=${storeKey} appIdSet=${appId.isNotEmpty()}")
runCatching { OpenIapStore(appContext, storeKey, appId) }
.getOrElse { OpenIapStore(appContext, "auto", appId) }
})
val purchases by iapStore.availablePurchases.collectAsState()
val status by iapStore.status.collectAsState()
val connectionStatus by iapStore.connectionStatus.collectAsState()
Expand Down Expand Up @@ -77,12 +84,12 @@ fun AvailablePurchasesScreen(
val restored = iapStore.restorePurchases()
iapStore.postStatusMessage(
message = "Restored ${restored.size} purchases",
status = PurchaseResultStatus.SUCCESS
status = PurchaseResultStatus.Success
)
} catch (e: Exception) {
iapStore.postStatusMessage(
message = e.message ?: "Restore failed",
status = PurchaseResultStatus.ERROR
status = PurchaseResultStatus.Error
)
}
}
Expand Down Expand Up @@ -195,7 +202,7 @@ fun AvailablePurchasesScreen(
// Check for unfinished transactions (purchases that need acknowledgment/consumption)
val unfinishedPurchases = purchases.filter { purchase ->
// TODO: In real implementation, check if purchase needs acknowledgment/consumption
// This would typically check: purchase.purchaseState == PURCHASED && !purchase.isAcknowledged
// This would typically check: purchase.purchaseState == PurchaseState.Purchased && !purchase.isAcknowledged
// For demo purposes, let's assume some consumable purchases might need finishing
(purchase.productId.contains("consumable", ignoreCase = true) ||
purchase.productId.contains("bulb", ignoreCase = true)) &&
Expand All @@ -218,21 +225,21 @@ fun AvailablePurchasesScreen(
if (ok) {
iapStore.postStatusMessage(
message = "Transaction finished successfully",
status = PurchaseResultStatus.SUCCESS,
status = PurchaseResultStatus.Success,
productId = purchase.productId
)
iapStore.getAvailablePurchases()
} else {
iapStore.postStatusMessage(
message = "Failed to finish transaction",
status = PurchaseResultStatus.ERROR,
status = PurchaseResultStatus.Error,
productId = purchase.productId
)
}
} catch (e: Exception) {
iapStore.postStatusMessage(
message = e.message ?: "Failed to finish transaction",
status = PurchaseResultStatus.ERROR,
status = PurchaseResultStatus.Error,
productId = purchase.productId
)
}
Expand Down Expand Up @@ -304,7 +311,7 @@ fun AvailablePurchasesScreen(
items(nonConsumables) { purchase ->
PurchaseItemCard(
purchase = purchase,
type = PurchaseType.NON_CONSUMABLE,
type = PurchaseType.NonConsumable,
onClick = { selectedPurchase = purchase }
)
}
Expand All @@ -319,7 +326,7 @@ fun AvailablePurchasesScreen(
items(consumables) { purchase ->
PurchaseItemCard(
purchase = purchase,
type = PurchaseType.CONSUMABLE,
type = PurchaseType.Consumable,
onClick = { selectedPurchase = purchase }
)
}
Expand Down Expand Up @@ -412,12 +419,12 @@ fun AvailablePurchasesScreen(
val restored = iapStore.restorePurchases()
iapStore.postStatusMessage(
message = "Restored ${restored.size} purchases",
status = PurchaseResultStatus.SUCCESS
status = PurchaseResultStatus.Success
)
} catch (e: Exception) {
iapStore.postStatusMessage(
message = e.message ?: "Restore failed",
status = PurchaseResultStatus.ERROR
status = PurchaseResultStatus.Error
)
}
}
Expand All @@ -444,9 +451,9 @@ fun AvailablePurchasesScreen(
}

enum class PurchaseType {
CONSUMABLE,
NON_CONSUMABLE,
SUBSCRIPTION
Consumable,
NonConsumable,
Subscription,
}

@Composable
Expand All @@ -456,17 +463,17 @@ fun PurchaseItemCard(
onClick: () -> Unit
) {
val (backgroundColor, iconColor, icon) = when (type) {
PurchaseType.SUBSCRIPTION -> Triple(
PurchaseType.Subscription -> Triple(
AppColors.secondary.copy(alpha = 0.1f),
AppColors.secondary,
Icons.Default.Autorenew
)
PurchaseType.NON_CONSUMABLE -> Triple(
PurchaseType.NonConsumable -> Triple(
AppColors.success.copy(alpha = 0.1f),
AppColors.success,
Icons.Default.CheckCircle
)
PurchaseType.CONSUMABLE -> Triple(
PurchaseType.Consumable -> Triple(
AppColors.warning.copy(alpha = 0.1f),
AppColors.warning,
Icons.Default.Schedule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
Expand Down Expand Up @@ -221,7 +222,7 @@ fun OfferCodeScreen(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
Icons.Default.HelpOutline,
Icons.AutoMirrored.Filled.HelpOutline,
contentDescription = null,
tint = AppColors.primary
)
Expand Down
Loading
Loading