Skip to content

Commit ae8821b

Browse files
committed
feat(horizon): add horizon support with flavor
1 parent 2daf6cf commit ae8821b

File tree

16 files changed

+1030
-44
lines changed

16 files changed

+1030
-44
lines changed

.github/workflows/ci-horizon.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: CI Horizon
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: ci-horizon-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
wrapper-validation:
16+
name: Validate Gradle Wrapper
17+
runs-on: ubuntu-latest
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
- name: Validate Wrapper
22+
uses: gradle/wrapper-validation-action@v2
23+
24+
horizon-build:
25+
name: Build Horizon flavors
26+
runs-on: ubuntu-latest
27+
needs: wrapper-validation
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
32+
- name: Set up Java 17
33+
uses: actions/setup-java@v4
34+
with:
35+
distribution: temurin
36+
java-version: '17'
37+
38+
- name: Set up Gradle
39+
uses: gradle/gradle-build-action@v2
40+
41+
- name: Set up Android SDK
42+
uses: android-actions/setup-android@v3
43+
44+
- name: Install required SDK packages
45+
run: |
46+
sdkmanager --install \
47+
"platform-tools" \
48+
"platforms;android-34" \
49+
"build-tools;34.0.0"
50+
yes | sdkmanager --licenses
51+
52+
- name: Build Horizon variants
53+
run: ./gradlew --stacktrace --no-daemon :openiap:assembleHorizonDebug :Example:assembleHorizonDebug

.vscode/launch.json

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@
22
"version": "0.2.0",
33
"configurations": [
44
{
5-
"name": "🚀 Android: Run Example (install & start)",
5+
"name": "🚀 Android: Run Example (Play)",
66
"type": "node",
77
"request": "launch",
88
"runtimeExecutable": "bash",
99
"runtimeArgs": [
1010
"-lc",
11-
"./gradlew :Example:installDebug && adb shell am start -n dev.hyo.martie/.MainActivity"
11+
"./gradlew :Example:installPlayDebug && adb shell am start -n dev.hyo.martie/.MainActivity"
12+
],
13+
"console": "integratedTerminal"
14+
},
15+
{
16+
"name": "🚀 Android: Run Example (Horizon)",
17+
"type": "node",
18+
"request": "launch",
19+
"runtimeExecutable": "bash",
20+
"runtimeArgs": [
21+
"-lc",
22+
"./gradlew -PEXAMPLE_OPENIAP_STORE=horizon \"-PEXAMPLE_HORIZON_APP_ID=${input:horizonAppId}\" :Example:installHorizonDebug && adb shell am start -n dev.hyo.martie/.MainActivity"
1223
],
1324
"console": "integratedTerminal"
1425
},
@@ -24,13 +35,24 @@
2435
"console": "integratedTerminal"
2536
},
2637
{
27-
"name": "🧱 Android: Assemble Example (debug)",
38+
"name": "🧱 Android: Assemble Example (Play Debug)",
39+
"type": "node",
40+
"request": "launch",
41+
"runtimeExecutable": "bash",
42+
"runtimeArgs": [
43+
"-lc",
44+
"./gradlew :Example:assemblePlayDebug"
45+
],
46+
"console": "integratedTerminal"
47+
},
48+
{
49+
"name": "🧱 Android: Assemble Example (Horizon Debug)",
2850
"type": "node",
2951
"request": "launch",
3052
"runtimeExecutable": "bash",
3153
"runtimeArgs": [
3254
"-lc",
33-
"./gradlew :Example:assembleDebug"
55+
"./gradlew -PEXAMPLE_OPENIAP_STORE=horizon \"-PEXAMPLE_HORIZON_APP_ID=${input:horizonAppId}\" :Example:assembleHorizonDebug"
3456
],
3557
"console": "integratedTerminal"
3658
},
@@ -67,5 +89,13 @@
6789
],
6890
"console": "integratedTerminal"
6991
}
92+
],
93+
"inputs": [
94+
{
95+
"id": "horizonAppId",
96+
"type": "promptString",
97+
"description": "Enter Horizon App ID (Meta) used when building the example",
98+
"default": ""
99+
}
70100
]
71101
}

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
"**/node_modules/": true
3636
},
3737
"gradle.nestedProjects": true,
38-
"gradle.javaDebug": true,
3938
"typescript.validate.enable": false,
4039
"javascript.validate.enable": false,
4140
"typescript.tsc.autoDetect": "off",
@@ -44,6 +43,7 @@
4443
"billingclient",
4544
"gson",
4645
"hyodotdev",
46+
"martie",
4747
"openiap",
4848
"skus"
4949
]

CONTRIBUTING.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,32 @@ cd openiap-google
2222
# Open in Android Studio (recommended)
2323
./scripts/open-android-studio.sh
2424

25-
# Or build from CLI
26-
./gradlew :openiap:assemble
25+
# Or build from CLI (Play flavor)
26+
./gradlew :openiap:assemblePlayDebug
2727

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

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

36+
### Horizon flavor testing
37+
38+
The Horizon build uses a dedicated product flavor that bundles Meta's billing compatibility SDK.
39+
40+
```bash
41+
# Assemble the Horizon flavor of the library
42+
./gradlew :openiap:assembleHorizonDebug
43+
44+
# Install the sample app (replace APP_ID with your Meta Horizon identifier)
45+
./gradlew -PEXAMPLE_OPENIAP_STORE=horizon "-PEXAMPLE_HORIZON_APP_ID=APP_ID" :Example:installHorizonDebug
46+
adb shell am start -n dev.hyo.martie/.MainActivity
47+
```
48+
49+
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.
50+
3651
## Generated Types
3752

3853
- 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.

Example/build.gradle.kts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,28 @@ android {
1717

1818
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
1919
vectorDrawables.useSupportLibrary = true
20+
val appId = (project.findProperty("EXAMPLE_HORIZON_APP_ID") as String?)
21+
?: (project.findProperty("EXAMPLE_OPENIAP_APP_ID") as String?)
22+
?: ""
23+
buildConfigField("String", "HORIZON_APP_ID", "\"${appId}\"")
24+
}
25+
26+
flavorDimensions += "store"
27+
28+
productFlavors {
29+
val storeOverride = (project.findProperty("EXAMPLE_OPENIAP_STORE") as String?)
30+
31+
create("play") {
32+
dimension = "store"
33+
val value = storeOverride ?: "play"
34+
buildConfigField("String", "OPENIAP_STORE", "\"${value}\"")
35+
}
36+
37+
create("horizon") {
38+
dimension = "store"
39+
val value = storeOverride ?: "horizon"
40+
buildConfigField("String", "OPENIAP_STORE", "\"${value}\"")
41+
}
2042
}
2143

2244
buildTypes {
@@ -44,6 +66,7 @@ android {
4466

4567
buildFeatures {
4668
compose = true
69+
buildConfig = true
4770
}
4871

4972
packaging {
Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
package dev.hyo.martie
22

33
object IapConstants {
4-
// App-defined SKU lists
5-
val INAPP_SKUS = listOf(
4+
private fun isHorizon(): Boolean =
5+
dev.hyo.martie.BuildConfig.OPENIAP_STORE.equals("horizon", ignoreCase = true)
6+
7+
private val HORIZON_INAPP = listOf(
68
"dev.hyo.martie.10bulbs",
7-
"dev.hyo.martie.30bulbs"
9+
"dev.hyo.martie.30bulbs",
10+
)
11+
private val HORIZON_SUBS = listOf(
12+
"dev.hyo.martie.premium",
813
)
914

10-
val SUBS_SKUS = listOf(
11-
"dev.hyo.martie.premium"
15+
private val PLAY_INAPP = listOf(
16+
"dev.hyo.martie.10bulbs",
17+
"dev.hyo.martie.30bulbs",
1218
)
13-
}
19+
private val PLAY_SUBS = listOf(
20+
"dev.hyo.martie.premium",
21+
)
22+
23+
val INAPP_SKUS: List<String>
24+
get() = if (isHorizon()) HORIZON_INAPP else PLAY_INAPP
1425

26+
val SUBS_SKUS: List<String>
27+
get() = if (isHorizon()) HORIZON_SUBS else PLAY_SUBS
28+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,12 @@ fun PurchaseFlowScreen(
4646
val activity = remember(context) { context.findActivity() }
4747
val uiScope = rememberCoroutineScope()
4848
val appContext = remember(context) { context.applicationContext }
49-
val iapStore = storeParam ?: remember(appContext) { OpenIapStore(appContext) }
49+
val iapStore = storeParam ?: remember(appContext) {
50+
val storeKey = dev.hyo.martie.BuildConfig.OPENIAP_STORE
51+
val appId = dev.hyo.martie.BuildConfig.HORIZON_APP_ID
52+
runCatching { OpenIapStore(appContext, storeKey, appId) }
53+
.getOrElse { OpenIapStore(appContext, "auto", appId) }
54+
}
5055
val products by iapStore.products.collectAsState()
5156
val purchases by iapStore.availablePurchases.collectAsState()
5257
val androidProducts = remember(products) { products.filterIsInstance<ProductAndroid>() }

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@ fun SubscriptionFlowScreen(
5959
val activity = remember(context) { context.findActivity() }
6060
val uiScope = rememberCoroutineScope()
6161
val appContext = remember(context) { context.applicationContext }
62-
val iapStore = storeParam ?: remember(appContext) { OpenIapStore(appContext) }
62+
val iapStore = storeParam ?: remember(appContext) {
63+
val storeKey = dev.hyo.martie.BuildConfig.OPENIAP_STORE
64+
val appId = dev.hyo.martie.BuildConfig.HORIZON_APP_ID
65+
runCatching { OpenIapStore(appContext, storeKey, appId) }
66+
.getOrElse { OpenIapStore(appContext, "auto", appId) }
67+
}
6368
val products by iapStore.products.collectAsState()
6469
val purchases by iapStore.availablePurchases.collectAsState()
6570
val androidProducts = remember(products) { products.filterIsInstance<ProductAndroid>() }

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ OpenIAP GMS is a modern, type-safe Kotlin library that simplifies Google Play in
2929
- 🔐 **Google Play Billing v8** - Latest billing library with enhanced security
3030
-**Kotlin Coroutines** - Modern async/await API
3131
- 🎯 **Type Safe** - Full Kotlin type safety with sealed classes
32+
- 🥽 **Meta Horizon OS Support** - Optional compatibility SDK integration alongside Play Billing
3233
- 🔄 **Real-time Events** - Purchase update and error listeners
3334
- 🧵 **Thread Safe** - Concurrent operations with proper synchronization
3435
- 📱 **Easy Integration** - Simple singleton pattern with context management
@@ -52,6 +53,21 @@ dependencies {
5253
}
5354
```
5455

56+
### Optional provider configuration
57+
58+
Set the target billing provider via `BuildConfig` fields (default is `play`). The library will also auto-detect Horizon hardware when `auto` is supplied.
59+
60+
```kotlin
61+
android {
62+
defaultConfig {
63+
buildConfigField("String", "OPENIAP_STORE", "\"auto\"") // play | horizon | auto
64+
buildConfigField("String", "HORIZON_APP_ID", "\"YOUR_APP_ID\"")
65+
}
66+
}
67+
```
68+
69+
The example app reads the same values via `EXAMPLE_OPENIAP_STORE` / `EXAMPLE_HORIZON_APP_ID` Gradle properties for quick testing.
70+
5571
Or `build.gradle`:
5672

5773
```groovy
@@ -156,6 +172,21 @@ class MainActivity : AppCompatActivity() {
156172
}
157173
```
158174

175+
## 🥽 Testing on Meta Horizon
176+
177+
The library exposes a dedicated `horizon` product flavor that bundles Meta's billing compatibility SDK. Build and install it with:
178+
179+
```bash
180+
# Compile the Horizon flavor of the library
181+
./gradlew :openiap:assembleHorizonDebug
182+
183+
# Install the sample app (replace APP_ID with your Horizon app id)
184+
./gradlew -PEXAMPLE_OPENIAP_STORE=horizon "-PEXAMPLE_HORIZON_APP_ID=APP_ID" :Example:installHorizonDebug
185+
adb shell am start -n dev.hyo.martie/.MainActivity
186+
```
187+
188+
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.
189+
159190
## 📚 API Reference
160191

161192
### Core Methods

openiap/build.gradle.kts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ plugins {
55
id("com.vanniktech.maven.publish")
66
}
77

8+
import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
9+
810
// Resolve version from either 'openIapVersion' or 'OPENIAP_VERSION' or fallback
911
val openIapVersion: String =
1012
(project.findProperty("openIapVersion")
@@ -20,6 +22,21 @@ android {
2022

2123
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
2224
consumerProguardFiles("consumer-rules.pro")
25+
buildConfigField("String", "OPENIAP_STORE", "\"play\"")
26+
buildConfigField("String", "HORIZON_APP_ID", "\"\"")
27+
}
28+
29+
flavorDimensions += "store"
30+
31+
productFlavors {
32+
create("play") {
33+
dimension = "store"
34+
buildConfigField("String", "OPENIAP_STORE", "\"play\"")
35+
}
36+
create("horizon") {
37+
dimension = "store"
38+
buildConfigField("String", "OPENIAP_STORE", "\"horizon\"")
39+
}
2340
}
2441

2542
buildTypes {
@@ -44,7 +61,9 @@ android {
4461
// Enable Compose for composables in this library (IapContext)
4562
buildFeatures {
4663
compose = true
64+
buildConfig = true
4765
}
66+
4867
}
4968

5069
dependencies {
@@ -53,6 +72,9 @@ dependencies {
5372

5473
// Google Play Billing Library (align with app/lib v8)
5574
api("com.android.billingclient:billing-ktx:8.0.0")
75+
76+
// Meta Horizon Billing compatibility client (only needed on the horizon flavor)
77+
add("horizonImplementation", "com.meta.horizon.billingclient.api:horizon-billing-compatibility:1.1.1")
5678

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

87110
pom {
88111
name.set("OpenIAP GMS")

0 commit comments

Comments
 (0)