diff --git a/CLAUDE.md b/CLAUDE.md
index 071f36e5..7d568904 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,8 +17,14 @@ openiap/
## Required Pre-Work
-- Before writing or editing anything, load and review the relevant `CONVENTION.md` file in the specific package directory
-- For cross-package changes, review conventions for all affected packages
+**CRITICAL**: Before writing or editing anything in a package, **ALWAYS** load and review the relevant `CONVENTION.md` file:
+
+- **GraphQL Types** (`packages/gql`): See [`packages/gql/CONVENTION.md`](packages/gql/CONVENTION.md)
+- **Android Library** (`packages/google`): See [`packages/google/CONVENTION.md`](packages/google/CONVENTION.md)
+- **Apple Library** (`packages/apple`): See [`packages/apple/CONVENTION.md`](packages/apple/CONVENTION.md)
+- **Documentation** (`packages/docs`): Follow conventions in this file (below)
+
+For cross-package changes, review conventions for all affected packages.
---
@@ -289,20 +295,20 @@ Before committing any changes:
### Android Function Naming Conventions
-- **Android-specific functions MUST have `Android` suffix**
-- **Cross-platform functions have NO suffix**
+**IMPORTANT**: See [`packages/google/CONVENTION.md`](packages/google/CONVENTION.md) for detailed Android naming conventions.
-#### Android Examples
+**Key Rule**: Since `packages/google` is Android-only, **DO NOT add `Android` suffix** to function names, even for Android-specific APIs.
**✅ Correct**:
```kotlin
-// Android-specific
-fun acknowledgePurchaseAndroid()
-fun consumePurchaseAndroid()
-fun getPackageNameAndroid()
+// Android-specific functions (no suffix needed)
+fun acknowledgePurchase()
+fun consumePurchase()
+fun getPackageName()
+fun buildModule(context: Context)
-// Cross-platform
+// Cross-platform API functions
fun initConnection()
fun fetchProducts()
fun requestPurchase()
@@ -311,10 +317,13 @@ fun requestPurchase()
**❌ Incorrect**:
```kotlin
-// Missing Android suffix
-fun acknowledgePurchase() // Should be acknowledgePurchaseAndroid()
+// Don't add Android suffix in Android-only package
+fun acknowledgePurchaseAndroid() // Wrong!
+fun buildModuleAndroid() // Wrong!
```
+**Exception**: Only use `Android` suffix for types that are part of a cross-platform API (e.g., `ProductAndroid`, `PurchaseAndroid` that contrast with iOS types).
+
---
## iOS Library (`packages/apple`)
diff --git a/packages/google/.github/workflows/ci-horizon.yml b/packages/google/.github/workflows/ci-horizon.yml
new file mode 100644
index 00000000..ebf13cd5
--- /dev/null
+++ b/packages/google/.github/workflows/ci-horizon.yml
@@ -0,0 +1,54 @@
+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: |
+ yes | sdkmanager --licenses
+ sdkmanager --install \
+ "platform-tools" \
+ "platforms;android-34" \
+ "build-tools;34.0.0"
+
+ - name: Build Horizon variants
+ working-directory: packages/google
+ run: ./gradlew --stacktrace --no-daemon :openiap:assembleHorizonDebug :Example:assembleHorizonDebug
diff --git a/packages/google/CONTRIBUTING.md b/packages/google/CONTRIBUTING.md
index 40ad7cf2..13445ac8 100644
--- a/packages/google/CONTRIBUTING.md
+++ b/packages/google/CONTRIBUTING.md
@@ -33,6 +33,58 @@ cd openiap-google
adb shell am start -n dev.hyo.martie/.MainActivity
```
+## Horizon OS Development (Meta Quest)
+
+This library supports both Google Play Store and Meta Horizon OS (Quest devices) using product flavors.
+
+### Setting Up Horizon App ID
+
+1. **Create `local.properties`** in the project root (if it doesn't exist):
+
+```properties
+# local.properties (DO NOT commit this file)
+sdk.dir=/path/to/Android/sdk
+
+# Horizon OS App ID (Meta Quest)
+EXAMPLE_HORIZON_APP_ID=your_horizon_app_id_here
+```
+
+1. **Alternative: Pass via command line**:
+
+```bash
+# Build with App ID
+./gradlew :Example:assembleHorizonDebug -PEXAMPLE_HORIZON_APP_ID=your_app_id
+
+# Install with App ID
+./gradlew :Example:installHorizonDebug -PEXAMPLE_HORIZON_APP_ID=your_app_id
+```
+
+1. **Using Android Studio**:
+ - Open **View > Tool Windows > Build Variants**
+ - Set **Example** module to **horizonDebug**
+ - Set **openiap** module to **horizonDebug**
+ - Run the app (App ID will be read from `local.properties`)
+
+### Build Variants
+
+- **playDebug** / **playRelease** - Google Play Store billing
+- **horizonDebug** / **horizonRelease** - Meta Horizon OS billing
+
+### Testing on Quest Devices
+
+```bash
+# Connect Quest via ADB
+adb devices
+
+# Install Horizon variant
+./gradlew :Example:installHorizonDebug
+
+# View logs
+adb logcat | grep -E "OpenIap|Horizon"
+```
+
+**Note**: The Horizon App ID is required for Horizon Billing to work. Without it, the billing client will fail to connect.
+
## Generated Types
- All GraphQL models in `openiap/src/main/java/dev/hyo/openiap/Types.kt` are generated from the [`openiap` monorepo](https://github.com/hyodotdev/openiap/tree/main/packages/gql). When you update API behavior, adjust the upstream type generator first so the Kotlin output stays in sync across platforms.
diff --git a/packages/google/CONVENTION.md b/packages/google/CONVENTION.md
index d542fff2..2fb8b6b1 100644
--- a/packages/google/CONVENTION.md
+++ b/packages/google/CONVENTION.md
@@ -2,6 +2,28 @@
## Naming Conventions
+### Android-Specific Functions
+
+**IMPORTANT**: Since this is an Android-only package, **DO NOT add `Android` suffix** to function names, even for Android-specific APIs.
+
+**✅ Correct**:
+```kotlin
+fun acknowledgePurchase()
+fun consumePurchase()
+fun getPackageName()
+fun buildModule(context: Context)
+fun isHorizonEnvironment(context: Context)
+```
+
+**❌ Incorrect**:
+```kotlin
+fun acknowledgePurchaseAndroid() // Don't add Android suffix
+fun consumePurchaseAndroid() // Don't add Android suffix
+fun buildModuleAndroid() // Don't add Android suffix
+```
+
+**Exception**: Only add `Android` suffix when the function is part of a cross-platform API that has platform-specific variants (e.g., `ProductAndroid`, `PurchaseAndroid` types that contrast with iOS types).
+
### Enum Values
- Enum values in this codebase must use **kebab-case** (e.g., `non-consumable`, `in-app`, `user-cancelled`)
- This matches the convention used in the auto-generated Types.kt from GraphQL schemas
diff --git a/packages/google/Example/build.gradle.kts b/packages/google/Example/build.gradle.kts
index 7acf0640..447a77f6 100644
--- a/packages/google/Example/build.gradle.kts
+++ b/packages/google/Example/build.gradle.kts
@@ -1,9 +1,18 @@
+import java.util.Properties
+
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("org.jetbrains.kotlin.plugin.compose")
}
+// Load local.properties
+val localProperties = Properties()
+val localPropertiesFile = rootProject.file("local.properties")
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.inputStream().use { localProperties.load(it) }
+}
+
android {
namespace = "dev.hyo.martie"
compileSdk = 34
@@ -17,6 +26,50 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
+
+ val appId = localProperties.getProperty("EXAMPLE_HORIZON_APP_ID")
+ ?: localProperties.getProperty("EXAMPLE_OPENIAP_APP_ID")
+ ?: (project.findProperty("EXAMPLE_HORIZON_APP_ID") as String?)
+ ?: (project.findProperty("EXAMPLE_OPENIAP_APP_ID") as String?)
+ ?: ""
+ buildConfigField("String", "HORIZON_APP_ID", "\"${appId}\"")
+ // Ensure placeholder exists for all variants (play included)
+ manifestPlaceholders["OCULUS_APP_ID"] = appId
+ }
+
+ flavorDimensions += "platform"
+
+ productFlavors {
+ // Auto flavor (default) - includes both libraries, detects platform at runtime
+ create("auto") {
+ dimension = "platform"
+ buildConfigField("String", "OPENIAP_STORE", "\"auto\"")
+ isDefault = true
+
+ // Dynamically inject OCULUS_APP_ID into AndroidManifest (needed for Horizon)
+ val appId = localProperties.getProperty("EXAMPLE_HORIZON_APP_ID")
+ ?: (project.findProperty("EXAMPLE_HORIZON_APP_ID") as String?)
+ ?: ""
+ manifestPlaceholders["OCULUS_APP_ID"] = appId
+ }
+
+ // Play flavor - Google Play Billing only
+ create("play") {
+ dimension = "platform"
+ buildConfigField("String", "OPENIAP_STORE", "\"play\"")
+ }
+
+ // Horizon flavor - Meta Horizon Billing only
+ create("horizon") {
+ dimension = "platform"
+ buildConfigField("String", "OPENIAP_STORE", "\"horizon\"")
+
+ // Dynamically inject OCULUS_APP_ID into AndroidManifest
+ val appId = localProperties.getProperty("EXAMPLE_HORIZON_APP_ID")
+ ?: (project.findProperty("EXAMPLE_HORIZON_APP_ID") as String?)
+ ?: ""
+ manifestPlaceholders["OCULUS_APP_ID"] = appId
+ }
}
buildTypes {
@@ -44,6 +97,7 @@ android {
buildFeatures {
compose = true
+ buildConfig = true
}
packaging {
diff --git a/packages/google/Example/src/horizon/AndroidManifest.xml b/packages/google/Example/src/horizon/AndroidManifest.xml
new file mode 100644
index 00000000..daba2856
--- /dev/null
+++ b/packages/google/Example/src/horizon/AndroidManifest.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/google/Example/src/main/AndroidManifest.xml b/packages/google/Example/src/main/AndroidManifest.xml
index e4494950..3e6d61b0 100644
--- a/packages/google/Example/src/main/AndroidManifest.xml
+++ b/packages/google/Example/src/main/AndroidManifest.xml
@@ -6,6 +6,11 @@
android:label="OpenIAP Example"
android:supportsRtl="true">
+
+
+
diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/Constants.kt b/packages/google/Example/src/main/java/dev/hyo/martie/Constants.kt
index 080261b6..c6f8102b 100644
--- a/packages/google/Example/src/main/java/dev/hyo/martie/Constants.kt
+++ b/packages/google/Example/src/main/java/dev/hyo/martie/Constants.kt
@@ -1,5 +1,7 @@
package dev.hyo.martie
+import android.os.Build
+
object IapConstants {
// App-defined SKU lists
val INAPP_SKUS = listOf(
@@ -8,13 +10,46 @@ object IapConstants {
"dev.hyo.martie.certified" // Non-consumable
)
- val SUBS_SKUS = listOf(
+ // Google Play: Two separate subscription products
+ private val SUBS_SKUS_PLAY = listOf(
"dev.hyo.martie.premium", // Main subscription with multiple offers
"dev.hyo.martie.premium_year" // Separate yearly subscription product
)
- // Base plan IDs for dev.hyo.martie.premium subscription
- const val PREMIUM_MONTHLY_BASE_PLAN = "premium" // Monthly base plan
- const val PREMIUM_YEARLY_BASE_PLAN = "premium-year" // Yearly base plan
+ // Horizon OS: Two separate SKUs (both at Level 1 to prevent auto-upgrade)
+ // IMPORTANT: In Horizon Developer Console, both must be set to the SAME Level
+ // to prevent automatic tier upgrades. Currently configured as:
+ // - premium (Level 1): Has MONTHLY and ANNUAL offers
+ // - premium_year (Level 1): Has ANNUAL offer only
+ private val SUBS_SKUS_HORIZON = listOf(
+ "dev.hyo.martie.premium", // Premium with multiple term options
+ "dev.hyo.martie.premium_year" // Separate yearly-only subscription
+ )
+
+ // Detect if running on Horizon OS at runtime
+ fun isHorizonOS(): Boolean {
+ return Build.MANUFACTURER.equals("Meta", ignoreCase = true) ||
+ Build.BRAND.equals("Meta", ignoreCase = true) ||
+ (Build.MODEL?.contains("Quest", ignoreCase = true) == true)
+ }
+
+ // Get subscription SKUs based on current device
+ fun getSubscriptionSkus(): List {
+ val isHorizon = isHorizonOS()
+ val skus = if (isHorizon) SUBS_SKUS_HORIZON else SUBS_SKUS_PLAY
+ println("IapConstants: getSubscriptionSkus() - isHorizon=$isHorizon, skus=$skus")
+ return skus
+ }
+
+ // Legacy: For screens that don't have platform context yet
+ val SUBS_SKUS = getSubscriptionSkus()
+
+ // Product IDs
+ const val PREMIUM_PRODUCT_ID = "dev.hyo.martie.premium"
+ const val PREMIUM_YEARLY_PRODUCT_ID_PLAY = "dev.hyo.martie.premium_year" // Play only
+
+ // Base plan IDs (used by both Play and Horizon)
+ const val PREMIUM_MONTHLY_BASE_PLAN = "premium" // 1 month plan
+ const val PREMIUM_YEARLY_BASE_PLAN = "premium-year" // 12 months plan
}
diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/MainActivity.kt b/packages/google/Example/src/main/java/dev/hyo/martie/MainActivity.kt
index c311567c..83cc3fca 100644
--- a/packages/google/Example/src/main/java/dev/hyo/martie/MainActivity.kt
+++ b/packages/google/Example/src/main/java/dev/hyo/martie/MainActivity.kt
@@ -14,34 +14,52 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import dev.hyo.martie.models.AppColors
import dev.hyo.martie.screens.*
+import dev.hyo.openiap.IapContext
+import dev.hyo.openiap.store.OpenIapStore
class MainActivity : ComponentActivity() {
+ // CRITICAL FIX: Create OpenIapStore at Activity level to persist across navigation
+ private val iapStore by lazy { OpenIapStore(applicationContext) }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
-
+
setContent {
OpenIapExampleTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = AppColors.background
) {
- AppNavigation()
+ AppNavigation(iapStore)
}
}
}
}
+
+ override fun onDestroy() {
+ // Clean up the store before lifecycleScope is cancelled
+ runCatching {
+ kotlinx.coroutines.runBlocking {
+ iapStore.endConnection()
+ }
+ }
+ super.onDestroy()
+ }
}
@Composable
-fun AppNavigation() {
+fun AppNavigation(store: OpenIapStore) {
val navController = rememberNavController()
val context = androidx.compose.ui.platform.LocalContext.current
+
val startRoute = remember {
val route = (context as? android.app.Activity)?.intent?.getStringExtra("openiap_route")
if (route in setOf("home", "purchase_flow", "subscription_flow", "available_purchases", "offer_code", "alternative_billing")) route!! else "home"
}
- NavHost(
+ // Provide the shared store to all screens via IapContext
+ IapContext.OpenIapProvider(store = store) {
+ NavHost(
navController = navController,
startDestination = startRoute
) {
@@ -73,6 +91,7 @@ fun AppNavigation() {
AlternativeBillingScreen(navController)
}
}
+ }
}
@Composable
diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt
index afa16d21..bd1f175d 100644
--- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt
+++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/AlternativeBillingScreen.kt
@@ -36,6 +36,7 @@ import dev.hyo.openiap.AlternativeBillingMode
import dev.hyo.openiap.AlternativeBillingModeAndroid
import dev.hyo.openiap.InitConnectionConfig
import dev.hyo.martie.util.findActivity
+import kotlinx.coroutines.delay
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -44,26 +45,33 @@ fun AlternativeBillingScreen(navController: NavController) {
val activity = remember(context) { context.findActivity() }
val appContext = remember(context) { context.applicationContext }
+ // Platform detection (runtime detection)
+ val isHorizon = remember { dev.hyo.martie.IapConstants.isHorizonOS() }
+
var selectedMode by remember { mutableStateOf(AlternativeBillingMode.ALTERNATIVE_ONLY) }
var isModeDropdownExpanded by remember { mutableStateOf(false) }
- // Initialize store - recreate when mode changes
- val iapStore = remember(selectedMode) {
- android.util.Log.d("AlternativeBillingScreen", "Creating new OpenIapStore with mode: $selectedMode")
+ // Initialize store - use default constructor for auto-detection (compatible with both Play and Horizon)
+ val iapStore = remember {
+ android.util.Log.d("AlternativeBillingScreen", "Creating OpenIapStore with auto-detection")
dev.hyo.openiap.OpenIapLog.isEnabled = true
- val store = OpenIapStore(appContext, alternativeBillingMode = selectedMode)
+ // Use default constructor which auto-detects platform (Play or Horizon)
+ // Alternative billing mode will be set via initConnection config
+ OpenIapStore(appContext)
+ }
- // Add event-based listener for User Choice Billing
+ // Set up User Choice Billing listener when mode changes
+ LaunchedEffect(selectedMode) {
if (selectedMode == AlternativeBillingMode.USER_CHOICE) {
- store.addUserChoiceBillingListener { details ->
+ iapStore.addUserChoiceBillingListener { details ->
android.util.Log.d("UserChoiceEvent", "=== User Choice Billing Event ===")
android.util.Log.d("UserChoiceEvent", "External Token: ${details.externalTransactionToken}")
android.util.Log.d("UserChoiceEvent", "Products: ${details.products}")
android.util.Log.d("UserChoiceEvent", "==============================")
// Show result in UI
- store.postStatusMessage(
+ iapStore.postStatusMessage(
message = "User selected alternative billing\nToken: ${details.externalTransactionToken.take(20)}...\nProducts: ${details.products.joinToString()}",
status = dev.hyo.openiap.store.PurchaseResultStatus.Info,
productId = details.products.firstOrNull()
@@ -72,9 +80,10 @@ fun AlternativeBillingScreen(navController: NavController) {
// TODO: Process payment with your payment system
// Then create token and report to backend
}
+ } else {
+ // Remove listener when not in USER_CHOICE mode
+ iapStore.setUserChoiceBillingListener(null)
}
-
- store
}
val products by iapStore.products.collectAsState()
@@ -105,6 +114,17 @@ fun AlternativeBillingScreen(navController: NavController) {
// Initialize connection when mode changes
LaunchedEffect(selectedMode) {
try {
+ android.util.Log.d("AlternativeBillingScreen", "Initializing with mode: $selectedMode")
+
+ // IMPORTANT: End existing connection first before creating new one
+ android.util.Log.d("AlternativeBillingScreen", "Ending existing connection...")
+ iapStore.endConnection()
+ delay(500) // Give it time to fully disconnect
+
+ // Set activity
+ iapStore.setActivity(activity)
+
+ // Create config based on selected mode
val config = when (selectedMode) {
AlternativeBillingMode.USER_CHOICE -> InitConnectionConfig(
alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice
@@ -115,16 +135,23 @@ fun AlternativeBillingScreen(navController: NavController) {
else -> null
}
+ android.util.Log.d("AlternativeBillingScreen", "Reconnecting with config: $config")
val connected = iapStore.initConnection(config)
+ android.util.Log.d("AlternativeBillingScreen", "Connection result: $connected")
+
if (connected) {
- iapStore.setActivity(activity)
+ android.util.Log.d("AlternativeBillingScreen", "Fetching products...")
val request = ProductRequest(
skus = IapConstants.INAPP_SKUS,
type = ProductQueryType.InApp
)
iapStore.fetchProducts(request)
+ } else {
+ android.util.Log.e("AlternativeBillingScreen", "Failed to connect to billing service")
}
- } catch (_: Exception) { }
+ } catch (e: Exception) {
+ android.util.Log.e("AlternativeBillingScreen", "Connection error: ${e.message}", e)
+ }
}
DisposableEffect(Unit) {
@@ -161,6 +188,46 @@ fun AlternativeBillingScreen(navController: NavController) {
contentPadding = PaddingValues(vertical = 20.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
+ // Horizon Info Banner (Alternative Billing is now supported!)
+ if (isHorizon) {
+ item {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ shape = RoundedCornerShape(12.dp),
+ colors = CardDefaults.cardColors(containerColor = AppColors.primary.copy(alpha = 0.1f))
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ Icons.Default.Info,
+ contentDescription = null,
+ tint = AppColors.primary,
+ modifier = Modifier.size(24.dp)
+ )
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ "Testing Meta Horizon Alternative Billing",
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Bold,
+ color = AppColors.primary
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ Text(
+ "Alternative Billing APIs are available through Horizon Billing Compatibility Library. Testing if they work correctly.",
+ style = MaterialTheme.typography.bodySmall,
+ color = AppColors.textSecondary
+ )
+ }
+ }
+ }
+ }
+ }
+
// Mode Selection Dropdown
item {
Card(
@@ -182,7 +249,9 @@ fun AlternativeBillingScreen(navController: NavController) {
ExposedDropdownMenuBox(
expanded = isModeDropdownExpanded,
- onExpandedChange = { isModeDropdownExpanded = it }
+ onExpandedChange = {
+ isModeDropdownExpanded = it
+ }
) {
OutlinedTextField(
value = when (selectedMode) {
diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt
index 808b78a5..1b9a9b72 100644
--- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt
+++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/PurchaseFlowScreen.kt
@@ -54,7 +54,9 @@ 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) {
+ OpenIapStore(appContext)
+ }
val products by iapStore.products.collectAsState()
val purchases by iapStore.availablePurchases.collectAsState()
val androidProducts = remember(products) { products.filterIsInstance() }
diff --git a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt
index fd19349e..8c996336 100644
--- a/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt
+++ b/packages/google/Example/src/main/java/dev/hyo/martie/screens/SubscriptionFlowScreen.kt
@@ -22,6 +22,7 @@ import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import dev.hyo.martie.models.AppColors
import dev.hyo.martie.IapConstants
+import dev.hyo.martie.BuildConfig
import dev.hyo.martie.screens.uis.*
import dev.hyo.openiap.IapContext
import dev.hyo.openiap.ProductAndroid
@@ -84,7 +85,9 @@ 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) {
+ OpenIapStore(appContext)
+ }
val products by iapStore.products.collectAsState()
val subscriptions by iapStore.subscriptions.collectAsState()
val purchases by iapStore.availablePurchases.collectAsState()
@@ -111,9 +114,20 @@ fun SubscriptionFlowScreen(
var subStatus by remember { mutableStateOf