Skip to content

Commit af03f0b

Browse files
authored
feat(android): horizon support (#3074)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Meta Quest Horizon OS support with an opt-in build configuration to enable Horizon billing. * **Improvements** * Refined purchase retrieval with enhanced in-memory filtering and cache-based type resolution. * Strengthened connection initialization with more robust activity availability handling. * **Documentation** * Added Horizon OS release notes and a getting-started setup guide with configuration and troubleshooting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 53bdf99 commit af03f0b

File tree

11 files changed

+370
-79
lines changed

11 files changed

+370
-79
lines changed

android/build.gradle

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ def getExtOrIntegerDefault(name) {
7070
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroIap_" + name]).toInteger()
7171
}
7272

73+
// Read horizonEnabled from gradle.properties, default to false (play)
74+
def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false
75+
7376
android {
7477
namespace "com.margelo.nitro.iap"
7578

@@ -83,6 +86,10 @@ android {
8386
// Ship consumer keep rules so Nitro HybridObjects aren't stripped in app release builds
8487
consumerProguardFiles 'consumer-rules.pro'
8588

89+
// Use horizonEnabled to determine platform flavor
90+
def flavor = horizonEnabled ? 'horizon' : 'play'
91+
missingDimensionStrategy "platform", flavor
92+
8693
externalNativeBuild {
8794
cmake {
8895
cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
@@ -172,13 +179,21 @@ dependencies {
172179
if (findProject(':react-native-nitro-modules') != null) {
173180
implementation project(":react-native-nitro-modules")
174181
}
175-
182+
176183
// Google Play Services
177184
implementation 'com.google.android.gms:play-services-base:18.5.0'
178-
185+
179186
// Kotlin coroutines
180187
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
181-
implementation "io.github.hyochan.openiap:openiap-google:${googleVersionString}"
188+
189+
// Determine which OpenIAP dependency to use based on horizonEnabled flag
190+
if (horizonEnabled) {
191+
// Use openiap-google-horizon for Meta Quest when horizonEnabled is true
192+
implementation "io.github.hyochan.openiap:openiap-google-horizon:${googleVersionString}"
193+
} else {
194+
// Use standard Google Play Billing
195+
implementation "io.github.hyochan.openiap:openiap-google:${googleVersionString}"
196+
}
182197
}
183198

184199
configurations.all {

android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,22 @@ class HybridRnIap : HybridRnIapSpec() {
6868
return@async true
6969
}
7070

71-
// Set current activity best-effort; don't fail init if missing
71+
// CRITICAL: Set Activity BEFORE calling initConnection
72+
// Horizon SDK needs Activity to initialize OVRPlatform with proper returnComponent
73+
// https://github.com/meta-quest/Meta-Spatial-SDK-Samples/issues/82#issuecomment-3452577530
7274
withContext(Dispatchers.Main) {
73-
runCatching { openIap.setActivity(context.currentActivity) }
75+
runCatching { context.currentActivity }
76+
.onSuccess { activity ->
77+
if (activity != null) {
78+
RnIapLog.debug("Activity available: ${activity.javaClass.name}")
79+
openIap.setActivity(activity)
80+
} else {
81+
RnIapLog.warn("Activity is null during initConnection")
82+
}
83+
}
84+
.onFailure {
85+
RnIapLog.warn("Activity not available during initConnection - OpenIAP will use Context")
86+
}
7487
}
7588

7689
// Single-flight: capture or create the shared Deferred atomically
@@ -432,12 +445,33 @@ class HybridRnIap : HybridRnIapSpec() {
432445
}
433446

434447
val result: List<OpenIapPurchase> = if (normalizedType != null) {
435-
val typeEnum = parseProductQueryType(normalizedType)
436448
RnIapLog.payload(
437449
"getAvailablePurchases.native",
438-
mapOf("type" to typeEnum.rawValue)
450+
mapOf("type" to normalizedType)
439451
)
440-
openIap.getAvailableItems(typeEnum)
452+
// OpenIAP's getAvailablePurchases doesn't support type filtering
453+
// Get all purchases and filter manually
454+
val allPurchases = openIap.getAvailablePurchases(null)
455+
456+
// Partition purchases to handle cases where product type is not yet cached
457+
val (knownTypePurchases, unknownTypePurchases) = allPurchases.partition {
458+
productTypeBySku.containsKey(it.productId)
459+
}
460+
461+
if (unknownTypePurchases.isNotEmpty()) {
462+
RnIapLog.warn(
463+
"getAvailablePurchases: Could not determine type for product IDs: " +
464+
"${unknownTypePurchases.map { it.productId }.joinToString()}. " +
465+
"These will be excluded from '$normalizedType' filtered lists. " +
466+
"Call fetchProducts first to populate product type cache."
467+
)
468+
}
469+
470+
when (normalizedType) {
471+
"in-app" -> knownTypePurchases.filter { productTypeBySku[it.productId] == "in-app" }
472+
"subs" -> knownTypePurchases.filter { productTypeBySku[it.productId] == "subs" }
473+
else -> allPurchases
474+
}
441475
} else {
442476
RnIapLog.payload("getAvailablePurchases.native", mapOf("type" to "all"))
443477
openIap.getAvailablePurchases(null)

docs/blog/2025-10-05-release-14.4.12-alternative-billing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
slug: release-14.4.12
2+
slug: release-14.4.12-alternative-billing
33
title: 14.4.12 - Alternative Billing Support
44
authors: [hyochan]
55
tags: [release, alternative-billing, ios, android, storekit, google-play]
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
slug: 14.4.33
3+
title: 14.4.33 - Horizon OS Support
4+
authors: [hyochan]
5+
tags: [release, horizon-os, android, meta-quest, vr]
6+
date: 2025-10-28
7+
---
8+
9+
# 14.4.33 Release Notes
10+
11+
![Horizon OS Support](/img/horizon.png)
12+
13+
React Native IAP 14.4.33 introduces **Horizon OS support** for Meta Quest devices, enabling developers to implement in-app purchases in VR applications using the same familiar API.
14+
15+
This release integrates Meta's Platform SDK for in-app purchases on Horizon OS, while maintaining the unified [OpenIAP](https://openiap.dev) interface across iOS, Android, and now Horizon OS.
16+
17+
👉 [View the 14.4.33 release](https://github.com/hyochan/react-native-iap/releases/tag/14.4.33)
18+
19+
<!-- truncate -->
20+
21+
## 🚀 Highlights
22+
23+
### Seamless Horizon OS Integration
24+
25+
React Native IAP now supports Meta Quest devices running Horizon OS with **zero code changes** required. Simply enable Horizon mode in your configuration, and your existing purchase code works seamlessly across all platforms.
26+
27+
**Key Features**:
28+
29+
- ✅ In-app purchases (consumable and non-consumable)
30+
- ✅ Subscriptions
31+
- ✅ Purchase restoration
32+
- ✅ Product fetching with localized pricing
33+
- ✅ Purchase verification
34+
- ✅ Same API as iOS and Android - no platform-specific code needed
35+
36+
### Configuration Setup
37+
38+
Enable Horizon OS support with two simple configuration steps:
39+
40+
**1. Enable Horizon mode** in `android/gradle.properties`:
41+
42+
```properties
43+
# Enable Horizon OS support (Meta Quest)
44+
horizonEnabled=true
45+
```
46+
47+
**2. Add Horizon App ID** to `android/app/src/main/AndroidManifest.xml`:
48+
49+
```xml
50+
<application>
51+
<!-- Meta Horizon App ID (required for Horizon OS) -->
52+
<meta-data
53+
android:name="com.meta.horizon.platform.ovr.OCULUS_APP_ID"
54+
android:value="YOUR_HORIZON_APP_ID" />
55+
</application>
56+
```
57+
58+
For detailed setup instructions, see the [Horizon OS Setup Guide](/docs/getting-started/setup-horizon).
59+
60+
The configuration automatically:
61+
62+
- Uses `openiap-google-horizon` artifact instead of `openiap-google`
63+
- Adds Horizon Platform SDK and Billing SDK dependencies
64+
- Configures your app with the Horizon App ID metadata
65+
66+
## 📦 Getting Started
67+
68+
To get started with Horizon OS integration:
69+
70+
1. **Install react-native-iap 14.4.33 or later**:
71+
72+
```bash
73+
npm install react-native-iap@14.4.33
74+
# or
75+
yarn add react-native-iap@14.4.33
76+
# or
77+
bun add react-native-iap@14.4.33
78+
```
79+
80+
2. **Follow the setup guide**: See the [Horizon OS Setup Guide](/docs/getting-started/setup-horizon) for detailed instructions on configuration, testing, and troubleshooting.
81+
82+
**No Breaking Changes**: All changes are additive. Existing apps will continue to work without modifications. Horizon support is opt-in via configuration.
83+
84+
## 🔗 References
85+
86+
- [Horizon OS Setup Guide](/docs/getting-started/setup-horizon)
87+
- [OpenIAP Documentation](https://openiap.dev)
88+
- [Meta Quest Developer Hub](https://developer.oculus.com/)
89+
90+
Questions or issues? Let us know via [GitHub issues](https://github.com/hyochan/react-native-iap/issues).
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
---
2+
sidebar_position: 4
3+
---
4+
5+
# Horizon OS
6+
7+
This guide covers setting up react-native-iap for Meta Quest devices running Horizon OS. Horizon OS uses Meta's Platform SDK for in-app purchases instead of Google Play Billing.
8+
9+
:::info OpenIAP Horizon Setup
10+
11+
For detailed Horizon OS setup instructions including environment configuration, SDK setup, and platform integration details, refer to the official OpenIAP documentation:
12+
13+
**[OpenIAP Horizon OS Setup Guide](https://www.openiap.dev/docs/horizon-setup)**
14+
15+
This guide focuses on react-native-iap specific configuration.
16+
17+
:::
18+
19+
## Prerequisites
20+
21+
- Meta Quest Developer account
22+
- App created in Meta Quest Developer Hub
23+
- Quest device or Quest Link for testing
24+
25+
## React Native IAP Configuration
26+
27+
:::tip Example Configuration
28+
29+
You can refer to the example app for a working configuration. See the commented sections in:
30+
31+
- [`example/android/gradle.properties`](https://github.com/hyochan/react-native-iap/blob/main/example/android/gradle.properties)
32+
- [`example/android/app/src/main/AndroidManifest.xml`](https://github.com/hyochan/react-native-iap/blob/main/example/android/app/src/main/AndroidManifest.xml)
33+
34+
:::
35+
36+
### 1. Enable Horizon Mode
37+
38+
Add `horizonEnabled=true` to your `android/gradle.properties`:
39+
40+
```properties
41+
# Enable Horizon OS support (Meta Quest)
42+
horizonEnabled=true
43+
```
44+
45+
### 2. Add Horizon App ID to AndroidManifest.xml
46+
47+
Add the Horizon App ID metadata to your `android/app/src/main/AndroidManifest.xml`:
48+
49+
```xml
50+
<application>
51+
<!-- Other configurations -->
52+
53+
<!-- Meta Horizon App ID (required for Horizon OS) -->
54+
<meta-data
55+
android:name="com.meta.horizon.platform.ovr.OCULUS_APP_ID"
56+
android:value="YOUR_HORIZON_APP_ID" />
57+
</application>
58+
```
59+
60+
Get your App ID from [Meta Quest Developer Hub](https://developer.oculus.com/).
61+
62+
### 3. Clean and Rebuild
63+
64+
After configuration, clean and rebuild your app:
65+
66+
```bash
67+
cd android
68+
./gradlew clean
69+
cd ..
70+
npx react-native run-android
71+
```
72+
73+
This will:
74+
75+
- Use `openiap-google-horizon` artifact instead of `openiap-google`
76+
- Add Horizon Platform SDK dependencies
77+
- Configure the app with your Horizon App ID
78+
79+
## Code Integration
80+
81+
The code integration for Horizon OS is identical to standard Android integration. react-native-iap handles the platform differences automatically.
82+
83+
Use the same `useIAP` hook and API methods as you would for Android or iOS. See the [Purchases Guide](../guides/purchases) for complete examples.
84+
85+
## Build Configuration
86+
87+
When you enable Horizon mode, react-native-iap automatically:
88+
89+
1. **Adds Dependencies**: Includes Horizon Platform SDK and Horizon Billing SDK
90+
2. **Uses Correct Artifact**: Switches to `openiap-google-horizon` instead of `openiap-google`
91+
3. **Configures AndroidManifest**: Your manually added Horizon App ID is used
92+
93+
You can verify the configuration by checking:
94+
95+
```bash
96+
# Check gradle.properties
97+
cat android/gradle.properties | grep horizonEnabled
98+
99+
# Check dependency tree
100+
cd android && ./gradlew :react-native-iap:dependencies --configuration debugRuntimeClasspath | grep openiap-google
101+
```
102+
103+
## Troubleshooting
104+
105+
### "Activity not available" Error
106+
107+
**Solution**: This was fixed in react-native-iap 14.4.31+. Update to the latest version:
108+
109+
```bash
110+
npm install react-native-iap@latest
111+
# or
112+
yarn add react-native-iap@latest
113+
```
114+
115+
### Product IDs Not Found
116+
117+
**Solutions**:
118+
119+
- Verify product IDs match in Meta Quest Developer Hub
120+
- Ensure products are published and active
121+
- Check that your Horizon App ID is correct in AndroidManifest.xml
122+
- Clean and rebuild: `cd android && ./gradlew clean`
123+
124+
### Wrong Artifact Being Used
125+
126+
**Solutions**:
127+
128+
- Check that `horizonEnabled=true` is in `android/gradle.properties`
129+
- Clean build: `cd android && ./gradlew clean`
130+
- Check build logs to verify correct artifact is being used
131+
132+
## Next Steps
133+
134+
- [Purchases Guide](../guides/purchases) - Learn how to implement purchases
135+
- [Error Codes](../api/error-codes) - Understand error handling
136+
- [OpenIAP Horizon Setup](https://www.openiap.dev/docs/horizon-setup) - Detailed platform setup

docs/sidebars.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ const sidebars: SidebarsConfig = {
1313
items: [
1414
'getting-started/installation',
1515
'getting-started/setup-ios',
16-
'getting-started/setup-android',
16+
{
17+
type: 'category',
18+
label: 'Android Setup',
19+
link: {type: 'doc', id: 'getting-started/setup-android'},
20+
items: ['getting-started/setup-horizon'],
21+
},
1722
],
1823
},
1924
{

docs/static/img/horizon.png

1.37 MB
Loading

example/android/app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
android:allowBackup="false"
1111
android:theme="@style/AppTheme"
1212
android:supportsRtl="true">
13+
14+
<!-- Horizon OS (Meta Quest) Configuration -->
15+
<!-- <meta-data
16+
android:name="com.meta.horizon.platform.ovr.OCULUS_APP_ID"
17+
android:value="31705015229097839" /> -->
18+
1319
<activity
1420
android:name=".MainActivity"
1521
android:label="@string/app_name"

example/android/gradle.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,8 @@ hermesEnabled=true
4242
# This allows your app to draw behind system bars for an immersive UI.
4343
# Note: Only works with ReactActivity and should not be used with custom Activity.
4444
edgeToEdgeEnabled=false
45+
46+
# Enable Horizon OS support for Meta Quest (set to true to use openiap-google-horizon)
47+
# When true, uses Meta's Platform SDK instead of Google Play Billing
48+
# Default: false (uses Google Play Billing)
49+
# horizonEnabled=true

0 commit comments

Comments
 (0)