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
53 changes: 53 additions & 0 deletions .github/workflows/ci-horizon.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI Horizon

on:
push:
pull_request:

permissions:
contents: read

concurrency:
group: ci-horizon-${{ github.ref }}
cancel-in-progress: true

jobs:
wrapper-validation:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate Wrapper
uses: gradle/wrapper-validation-action@v2

horizon-build:
name: Build Horizon flavors
runs-on: ubuntu-latest
needs: wrapper-validation
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Set up Gradle
uses: gradle/gradle-build-action@v2

- name: Set up Android SDK
uses: android-actions/setup-android@v3

- name: Install required SDK packages
run: |
sdkmanager --install \
"platform-tools" \
"platforms;android-34" \
"build-tools;34.0.0"
yes | sdkmanager --licenses

- name: Build Horizon variants
run: ./gradlew --stacktrace --no-daemon :openiap:assembleHorizonDebug :Example:assembleHorizonDebug
38 changes: 34 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@
"version": "0.2.0",
"configurations": [
{
"name": "🚀 Android: Run Example (install & start)",
"name": "🚀 Android: Run Example (Play)",
"type": "node",
"request": "launch",
"runtimeExecutable": "bash",
"runtimeArgs": [
"-lc",
"./gradlew :Example:installDebug && adb shell am start -n dev.hyo.martie/.MainActivity"
"./gradlew :Example:installPlayDebug && adb shell am start -n dev.hyo.martie/.MainActivity"
],
"console": "integratedTerminal"
},
{
"name": "🚀 Android: Run Example (Horizon)",
"type": "node",
"request": "launch",
"runtimeExecutable": "bash",
"runtimeArgs": [
"-lc",
"./gradlew -PEXAMPLE_OPENIAP_STORE=horizon \"-PEXAMPLE_HORIZON_APP_ID=${input:horizonAppId}\" :Example:installHorizonDebug && adb shell am start -n dev.hyo.martie/.MainActivity"
],
"console": "integratedTerminal"
},
Expand All @@ -24,13 +35,24 @@
"console": "integratedTerminal"
},
{
"name": "🧱 Android: Assemble Example (debug)",
"name": "🧱 Android: Assemble Example (Play Debug)",
"type": "node",
"request": "launch",
"runtimeExecutable": "bash",
"runtimeArgs": [
"-lc",
"./gradlew :Example:assemblePlayDebug"
],
"console": "integratedTerminal"
},
{
"name": "🧱 Android: Assemble Example (Horizon Debug)",
"type": "node",
"request": "launch",
"runtimeExecutable": "bash",
"runtimeArgs": [
"-lc",
"./gradlew :Example:assembleDebug"
"./gradlew -PEXAMPLE_OPENIAP_STORE=horizon \"-PEXAMPLE_HORIZON_APP_ID=${input:horizonAppId}\" :Example:assembleHorizonDebug"
],
"console": "integratedTerminal"
},
Expand Down Expand Up @@ -67,5 +89,13 @@
],
"console": "integratedTerminal"
}
],
"inputs": [
{
"id": "horizonAppId",
"type": "promptString",
"description": "Enter Horizon App ID (Meta) used when building the example",
"default": ""
}
]
}
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
"**/node_modules/": true
},
"gradle.nestedProjects": true,
"gradle.javaDebug": true,
"typescript.validate.enable": false,
"javascript.validate.enable": false,
"typescript.tsc.autoDetect": "off",
Expand All @@ -44,6 +43,7 @@
"billingclient",
"gson",
"hyodotdev",
"martie",
"openiap",
"skus"
]
Expand Down
23 changes: 19 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,32 @@ cd openiap-google
# Open in Android Studio (recommended)
./scripts/open-android-studio.sh

# Or build from CLI
./gradlew :openiap:assemble
# Or build from CLI (Play flavor)
./gradlew :openiap:assemblePlayDebug

# Run unit tests for the library module
./gradlew :openiap:test

# (Optional) Install and run the Example app
./gradlew :Example:installDebug
# (Optional) Install and run the Example app (Play flavor)
./gradlew :Example:installPlayDebug
adb shell am start -n dev.hyo.martie/.MainActivity
```

### Horizon flavor testing

The Horizon build uses a dedicated product flavor that bundles Meta's billing compatibility SDK.

```bash
# Assemble the Horizon flavor of the library
./gradlew :openiap:assembleHorizonDebug

# Install the sample app (replace APP_ID with your Meta Horizon identifier)
./gradlew -PEXAMPLE_OPENIAP_STORE=horizon "-PEXAMPLE_HORIZON_APP_ID=APP_ID" :Example:installHorizonDebug
adb shell am start -n dev.hyo.martie/.MainActivity
```

With flavors enabled, Gradle no longer creates the generic `installDebug`/`assembleDebug` tasks—run the flavor-specific tasks explicitly in scripts, CI pipelines, and IDE run configurations.

## Generated Types

- All GraphQL models in `openiap/src/main/java/dev/hyo/openiap/Types.kt` are generated from the [`hyodotdev/openiap-gql`](https://github.com/hyodotdev/openiap-gql) repository. When you update API behavior, adjust the upstream type generator first so the Kotlin output stays in sync across platforms.
Expand Down
23 changes: 23 additions & 0 deletions Example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,28 @@ android {

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
val appId = (project.findProperty("EXAMPLE_HORIZON_APP_ID") as String?)
?: (project.findProperty("EXAMPLE_OPENIAP_APP_ID") as String?)
?: ""
buildConfigField("String", "HORIZON_APP_ID", "\"${appId}\"")
}

flavorDimensions += "store"

productFlavors {
val storeOverride = (project.findProperty("EXAMPLE_OPENIAP_STORE") as String?)

create("play") {
dimension = "store"
val value = storeOverride ?: "play"
buildConfigField("String", "OPENIAP_STORE", "\"${value}\"")
}

create("horizon") {
dimension = "store"
val value = storeOverride ?: "horizon"
buildConfigField("String", "OPENIAP_STORE", "\"${value}\"")
}
}

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

buildFeatures {
compose = true
buildConfig = true
}

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

object IapConstants {
// App-defined SKU lists
val INAPP_SKUS = listOf(
"dev.hyo.martie.10bulbs",
"dev.hyo.martie.30bulbs",
Expand All @@ -17,4 +16,3 @@ object IapConstants {
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 @@ -54,7 +54,12 @@ fun PurchaseFlowScreen(
val activity = remember(context) { context.findActivity() }
val uiScope = rememberCoroutineScope()
val appContext = remember(context) { context.applicationContext }
val iapStore = storeParam ?: remember(appContext) { OpenIapStore(appContext) }
val iapStore = storeParam ?: remember(appContext) {
val storeKey = dev.hyo.martie.BuildConfig.OPENIAP_STORE
val appId = dev.hyo.martie.BuildConfig.HORIZON_APP_ID
runCatching { OpenIapStore(appContext, storeKey, appId) }
.getOrElse { OpenIapStore(appContext, "auto", appId) }
}
val products by iapStore.products.collectAsState()
val purchases by iapStore.availablePurchases.collectAsState()
val androidProducts = remember(products) { products.filterIsInstance<ProductAndroid>() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ fun SubscriptionFlowScreen(

// SharedPreferences to track current offer (necessary since Google doesn't provide offer info)
val prefs = remember { context.getSharedPreferences(SUBSCRIPTION_PREFS_NAME, Context.MODE_PRIVATE) }
val iapStore = storeParam ?: remember(appContext) { OpenIapStore(appContext) }
val iapStore = storeParam ?: remember(appContext) {
val storeKey = dev.hyo.martie.BuildConfig.OPENIAP_STORE
val appId = dev.hyo.martie.BuildConfig.HORIZON_APP_ID
runCatching { OpenIapStore(appContext, storeKey, appId) }
.getOrElse { OpenIapStore(appContext, "auto", appId) }
}
val products by iapStore.products.collectAsState()
val subscriptions by iapStore.subscriptions.collectAsState()
val purchases by iapStore.availablePurchases.collectAsState()
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ OpenIAP GMS is a modern, type-safe Kotlin library that simplifies Google Play in
- 🔐 **Google Play Billing v8** - Latest billing library with enhanced security
- ⚡ **Kotlin Coroutines** - Modern async/await API
- 🎯 **Type Safe** - Full Kotlin type safety with sealed classes
- 🥽 **Meta Horizon OS Support** - Optional compatibility SDK integration alongside Play Billing
- 🔄 **Real-time Events** - Purchase update and error listeners
- 🧵 **Thread Safe** - Concurrent operations with proper synchronization
- 📱 **Easy Integration** - Simple singleton pattern with context management
Expand All @@ -52,6 +53,21 @@ dependencies {
}
```

### Optional provider configuration

Set the target billing provider via `BuildConfig` fields (default is `play`). The library will also auto-detect Horizon hardware when `auto` is supplied.

```kotlin
android {
defaultConfig {
buildConfigField("String", "OPENIAP_STORE", "\"auto\"") // play | horizon | auto
buildConfigField("String", "HORIZON_APP_ID", "\"YOUR_APP_ID\"")
}
}
```

The example app reads the same values via `EXAMPLE_OPENIAP_STORE` / `EXAMPLE_HORIZON_APP_ID` Gradle properties for quick testing.

Or `build.gradle`:

```groovy
Expand Down Expand Up @@ -156,6 +172,21 @@ class MainActivity : AppCompatActivity() {
}
```

## 🥽 Testing on Meta Horizon

The library exposes a dedicated `horizon` product flavor that bundles Meta's billing compatibility SDK. Build and install it with:

```bash
# Compile the Horizon flavor of the library
./gradlew :openiap:assembleHorizonDebug

# Install the sample app (replace APP_ID with your Horizon app id)
./gradlew -PEXAMPLE_OPENIAP_STORE=horizon "-PEXAMPLE_HORIZON_APP_ID=APP_ID" :Example:installHorizonDebug
adb shell am start -n dev.hyo.martie/.MainActivity
```

For standard Google Play workflows, run the matching `play` tasks (`:openiap:assemblePlayDebug`, `:Example:installPlayDebug`). Flavors remove the generic `installDebug` task, so always target the desired flavor explicitly when using the CLI, CI, or IDE run configurations.

## 📚 API Reference

### Core Methods
Expand Down
23 changes: 23 additions & 0 deletions openiap/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ plugins {
id("com.vanniktech.maven.publish")
}

import com.vanniktech.maven.publish.AndroidSingleVariantLibrary

// Resolve version from either 'openIapVersion' or 'OPENIAP_VERSION' or fallback
val openIapVersion: String =
(project.findProperty("openIapVersion")
Expand All @@ -20,6 +22,21 @@ android {

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
buildConfigField("String", "OPENIAP_STORE", "\"play\"")
buildConfigField("String", "HORIZON_APP_ID", "\"\"")
}

flavorDimensions += "store"

productFlavors {
create("play") {
dimension = "store"
buildConfigField("String", "OPENIAP_STORE", "\"play\"")
}
create("horizon") {
dimension = "store"
buildConfigField("String", "OPENIAP_STORE", "\"horizon\"")
}
}

buildTypes {
Expand All @@ -44,7 +61,9 @@ android {
// Enable Compose for composables in this library (IapContext)
buildFeatures {
compose = true
buildConfig = true
}

}

dependencies {
Expand All @@ -53,6 +72,9 @@ dependencies {

// Google Play Billing Library (align with app/lib v8)
api("com.android.billingclient:billing-ktx:8.0.0")

// Meta Horizon Billing compatibility client (only needed on the horizon flavor)
add("horizonImplementation", "com.meta.horizon.billingclient.api:horizon-billing-compatibility:1.1.1")

// Kotlin Coroutines
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
Expand Down Expand Up @@ -83,6 +105,7 @@ mavenPublishing {
// Use the new Central Portal publishing which avoids Nexus staging profile lookups.
publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL)
signAllPublications()
configure(AndroidSingleVariantLibrary(variant = "playRelease", sourcesJar = true, publishJavadocJar = true))

pom {
name.set("OpenIAP GMS")
Expand Down
Loading
Loading