Skip to content

Commit ddbb342

Browse files
authored
feat(android): horizon support (#583)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Horizon OS support for Meta Quest with Play/Horizon build flavors and runtime selection. * **Documentation** * Added Horizon OS guides, updated Android setup, changelog entry, and example README with build/run steps. * **Chores** * Gradle flavor and dependency updates, example manifest/placeholders for Horizon App ID, IDE launch configs, version bumps, removed obsolete podspec. * **Tests** * Updated tests to supply mandatory iOS introductory price mode. * **Bug Fixes** * Default to Android path when no billing platform detected. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 961fbff commit ddbb342

File tree

26 files changed

+687
-160
lines changed

26 files changed

+687
-160
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ build/
1010
.claude/
1111

1212
.idea/
13+
!.idea/runConfigurations/
1314
flutter_inapp.iml
1415
flutter_inapp_android.iml
1516
flutter_export_environment*

.vscode/launch.json

Lines changed: 12 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
{
2-
// Use IntelliSense to learn about possible attributes.
3-
// Hover to view descriptions of existing attributes.
4-
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
52
"version": "0.2.0",
63
"configurations": [
74
{
5+
"name": "start documentation",
86
"type": "node",
97
"request": "launch",
10-
"name": "Start Docusaurus",
118
"cwd": "${workspaceFolder}/docs",
129
"runtimeExecutable": "npm",
1310
"runtimeArgs": ["run", "start"],
@@ -18,92 +15,46 @@
1815
}
1916
},
2017
{
21-
"type": "node",
22-
"request": "launch",
23-
"name": "Build Docusaurus",
24-
"cwd": "${workspaceFolder}/docs",
25-
"runtimeExecutable": "npm",
26-
"runtimeArgs": ["run", "build"],
27-
"console": "integratedTerminal",
28-
"internalConsoleOptions": "neverOpen"
29-
},
30-
{
31-
"type": "node",
32-
"request": "launch",
33-
"name": "Serve Docusaurus Build",
34-
"cwd": "${workspaceFolder}/docs",
35-
"runtimeExecutable": "npm",
36-
"runtimeArgs": ["run", "serve"],
37-
"console": "integratedTerminal",
38-
"internalConsoleOptions": "neverOpen"
39-
},
40-
{
41-
"name": "Open Xcode",
42-
"type": "node",
43-
"request": "launch",
44-
"runtimeExecutable": "/usr/bin/open",
45-
"runtimeArgs": ["-a", "Xcode"],
46-
"console": "internalConsole",
47-
"internalConsoleOptions": "openOnSessionStart"
48-
},
49-
{
50-
"name": "Open Android Studio",
18+
"name": "open android studio",
5119
"type": "node",
5220
"request": "launch",
5321
"runtimeExecutable": "/usr/bin/open",
54-
"runtimeArgs": ["-a", "Android Studio"],
22+
"runtimeArgs": ["-a", "Android Studio", "${workspaceFolder}/example/android"],
5523
"console": "internalConsole",
5624
"internalConsoleOptions": "openOnSessionStart"
5725
},
5826
{
59-
"name": "Flutter: Attach to Device",
60-
"type": "dart",
61-
"request": "attach"
62-
},
63-
{
64-
"name": "Flutter: Run Example App",
65-
"type": "dart",
66-
"request": "launch",
67-
"program": "lib/main.dart",
68-
"cwd": "${workspaceFolder}/example",
69-
"args": []
70-
},
71-
{
72-
"name": "Flutter: Run Example App (Profile Mode)",
27+
"name": "run ios",
7328
"type": "dart",
7429
"request": "launch",
7530
"program": "lib/main.dart",
7631
"cwd": "${workspaceFolder}/example",
77-
"flutterMode": "profile",
78-
"args": []
32+
"args": ["-d", "ios"]
7933
},
8034
{
81-
"name": "Flutter: Run Example App (Release Mode)",
35+
"name": "run ios (device)",
8236
"type": "dart",
8337
"request": "launch",
8438
"program": "lib/main.dart",
8539
"cwd": "${workspaceFolder}/example",
8640
"flutterMode": "release",
87-
"args": []
41+
"args": ["-d", "ios"]
8842
},
8943
{
90-
"name": "Example (Flutter Pixel 2)",
44+
"name": "run android",
9145
"type": "dart",
9246
"request": "launch",
9347
"program": "lib/main.dart",
9448
"cwd": "${workspaceFolder}/example",
95-
"deviceId": "HT79F1A00473",
96-
"args": []
49+
"args": ["--flavor", "play"]
9750
},
9851
{
99-
"name": "Example (iOS iPhone)",
52+
"name": "run horizon",
10053
"type": "dart",
10154
"request": "launch",
10255
"program": "lib/main.dart",
10356
"cwd": "${workspaceFolder}/example",
104-
"deviceId": "00008101-0006455036D8001E",
105-
"args": []
57+
"args": ["--flavor", "horizon"]
10658
}
107-
],
108-
"compounds": []
59+
]
10960
}

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# CHANGELOG
22

3+
## 7.1.13
4+
5+
- **feat**: Add Horizon OS support for Meta Quest devices
6+
- Added product flavor support for Meta Horizon billing (play/horizon flavors)
7+
- Integrated `openiap-google-horizon` dependency for Meta Quest Platform SDK
8+
- Added Horizon App ID configuration via gradle.properties and AndroidManifest.xml
9+
- Example app now supports both Google Play and Meta Horizon builds
10+
- Added comprehensive documentation for Horizon OS setup
11+
- [Blog post: Horizon OS Support](https://hyochan.github.io/flutter_inapp_purchase/blog/horizon-os-support)
12+
- [Setup guide: Horizon OS](https://hyochan.github.io/flutter_inapp_purchase/docs/getting-started/setup-horizon)
13+
- **BREAKING**: `introductoryPricePaymentModeIOS` is now required (non-nullable) in `ProductSubscriptionIOS`
14+
- Changed from `PaymentModeIOS?` to `PaymentModeIOS` (required field)
15+
- Default value is `PaymentModeIOS.Empty` when not provided
16+
- Update: Modified `_parsePaymentMode` helper to return `PaymentModeIOS.Empty` instead of `null`
17+
- **Migration**: Add `introductoryPricePaymentModeIOS: PaymentModeIOS.Empty` when creating `ProductSubscriptionIOS` instances manually
18+
319
## 7.1.12
420

521
- Update openiap-versions.

android/build.gradle

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,16 @@ android {
5656
}
5757
compileSdkVersion 34
5858

59+
// Read horizonEnabled from gradle.properties, default to false (play)
60+
def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false
61+
5962
defaultConfig {
6063
minSdkVersion 21
6164
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
65+
66+
// Use horizonEnabled to determine platform flavor
67+
def flavor = horizonEnabled ? 'horizon' : 'play'
68+
missingDimensionStrategy "platform", flavor
6269
}
6370
sourceSets {
6471
main.java.srcDirs += 'src/main/kotlin'
@@ -74,16 +81,41 @@ android {
7481
kotlinOptions {
7582
jvmTarget = '17'
7683
}
84+
85+
flavorDimensions "platform"
86+
productFlavors {
87+
// Play flavor - Google Play Billing (default)
88+
play {
89+
dimension "platform"
90+
isDefault = true
91+
}
92+
// Horizon flavor - Meta Horizon Billing
93+
horizon {
94+
dimension "platform"
95+
}
96+
}
7797
}
7898

7999
dependencies {
80-
// OpenIAP Google billing wrapper includes Google Play Billing
81-
implementation "io.github.hyochan.openiap:openiap-google:${openiapGoogleVersion}"
100+
// Determine which OpenIAP dependency to use based on horizonEnabled flag
101+
def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false
102+
if (horizonEnabled) {
103+
// Use openiap-google-horizon for Meta Quest when horizonEnabled is true
104+
implementation "io.github.hyochan.openiap:openiap-google-horizon:${openiapGoogleVersion}"
105+
} else {
106+
// Use standard Google Play Billing
107+
implementation "io.github.hyochan.openiap:openiap-google:${openiapGoogleVersion}"
108+
}
82109

83110
// For local debugging with a checked-out module, you can switch to:
84-
// debugImplementation project(":openiap")
85-
// and include the module in android/settings.gradle (see CONTRIBUTING.md)
111+
// implementation project(":openiap")
112+
// and uncomment the include in android/settings.gradle
86113

114+
// Amazon IAP only for play flavor (legacy support)
87115
implementation files('jars/in-app-purchasing-2.0.76.jar')
88116
implementation 'androidx.annotation:annotation:1.6.0'
117+
118+
// Google Play Billing for direct API usage (only in play flavor)
119+
// Note: This is already included in openiap, but kept for backward compatibility
120+
add("playCompileOnly", "com.android.billingclient:billing-ktx:8.0.0")
89121
}

android/settings.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ rootProject.name = 'flutter_inapp_purchase'
44
// 1) git clone https://github.com/hyodotdev/openiap-google
55
// 2) Update path below to your local checkout
66
// 3) In android/build.gradle, switch dependency to:
7-
// debugImplementation project(":openiap")
8-
// (and keep release on the Maven artifact)
7+
// implementation project(":openiap")
8+
// (and comment out the Maven Central dependency)
99
//
1010
// include ':openiap'
11-
// project(':openiap').projectDir = new File('/Users/you/path/to/openiap-google/openiap')
11+
// project(':openiap').projectDir = new File('/path/to/openiap/packages/google/openiap')

android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act
671671
safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.")
672672
return@launch
673673
}
674-
val purchases = iap.getAvailableItems(reqType)
674+
val purchases = iap.getAvailablePurchases(null)
675675
val arr = purchasesToJsonArray(purchases)
676676
safe.success(arr.toString())
677677
} catch (e: Exception) {
@@ -710,7 +710,11 @@ class AndroidInappPurchasePlugin internal constructor() : MethodCallHandler, Act
710710
safe.error(OpenIapError.NotPrepared.CODE, OpenIapError.NotPrepared.MESSAGE, "IAP module not initialized.")
711711
return@launch
712712
}
713-
val purchases = iap.getAvailableItems(reqType)
713+
// Note: As of v6.4.6+, getAvailablePurchases returns only active purchases on Android.
714+
// Purchase history (including expired/consumed items) is not supported on Android
715+
// by the OpenIAP library. The reqType parameter is preserved for backward compatibility
716+
// but is not used. Apps should migrate to getAvailableItems() for active purchases.
717+
val purchases = iap.getAvailablePurchases(null)
714718
val arr = purchasesToJsonArray(purchases)
715719
safe.success(arr.toString())
716720
} catch (e: Exception) {

android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/BillingError.kt

Lines changed: 0 additions & 50 deletions
This file was deleted.

android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/FlutterInappPurchasePlugin.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,24 @@ class FlutterInappPurchasePlugin : FlutterPlugin, ActivityAware {
3131
isAmazon = false
3232
}
3333
}
34+
35+
// If neither Play Store nor Amazon is detected, default to Android (for Horizon and other stores)
36+
// This allows openiap to handle different billing implementations via flavors
37+
if (!isAndroid && !isAmazon) {
38+
android.util.Log.i("FlutterInappPurchase", "No Play Store or Amazon detected - defaulting to Android plugin (supports Horizon and other stores)")
39+
isAndroid = true
40+
}
41+
3442
channel = MethodChannel(messenger, "flutter_inapp")
3543
if (isAndroid) {
44+
android.util.Log.i("FlutterInappPurchase", "Initializing Android IAP plugin")
3645
val plugin = AndroidInappPurchasePlugin()
3746
plugin.setContext(context)
3847
plugin.setChannel(channel)
3948
androidInappPurchasePlugin = plugin
4049
channel!!.setMethodCallHandler(plugin)
4150
} else if (isAmazon) {
51+
android.util.Log.i("FlutterInappPurchase", "Initializing Amazon IAP plugin")
4252
amazonInappPurchasePlugin = AmazonInappPurchasePlugin()
4353
amazonInappPurchasePlugin!!.setContext(context)
4454
amazonInappPurchasePlugin!!.setChannel(channel)

0 commit comments

Comments
 (0)