(AlternativeBillingMode.ALTERNATIVE_ONLY) }
+ var selectedMode by remember { mutableStateOf(BillingModeOption.BILLING_PROGRAMS) }
var isModeDropdownExpanded by remember { mutableStateOf(false) }
+ var selectedBillingProgram by remember { mutableStateOf(BillingProgramAndroid.ExternalOffer) }
// Initialize store - use default constructor for auto-detection (compatible with both Play and Horizon)
val iapStore = remember {
@@ -63,7 +75,7 @@ fun AlternativeBillingScreen(navController: NavController) {
// Set up User Choice Billing listener when mode changes
LaunchedEffect(selectedMode) {
- if (selectedMode == AlternativeBillingMode.USER_CHOICE) {
+ if (selectedMode == BillingModeOption.USER_CHOICE) {
iapStore.addUserChoiceBillingListener { details ->
android.util.Log.d("UserChoiceEvent", "=== User Choice Billing Event ===")
android.util.Log.d("UserChoiceEvent", "External Token: ${details.externalTransactionToken}")
@@ -112,7 +124,7 @@ fun AlternativeBillingScreen(navController: NavController) {
}
// Initialize connection when mode changes
- LaunchedEffect(selectedMode) {
+ LaunchedEffect(selectedMode, selectedBillingProgram) {
try {
android.util.Log.d("AlternativeBillingScreen", "Initializing with mode: $selectedMode")
@@ -126,13 +138,18 @@ fun AlternativeBillingScreen(navController: NavController) {
// Create config based on selected mode
val config = when (selectedMode) {
- AlternativeBillingMode.USER_CHOICE -> InitConnectionConfig(
+ BillingModeOption.USER_CHOICE -> InitConnectionConfig(
alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice
)
- AlternativeBillingMode.ALTERNATIVE_ONLY -> InitConnectionConfig(
+ BillingModeOption.ALTERNATIVE_ONLY -> InitConnectionConfig(
alternativeBillingModeAndroid = AlternativeBillingModeAndroid.AlternativeOnly
)
- else -> null
+ BillingModeOption.BILLING_PROGRAMS -> {
+ // For 8.2.0+ Billing Programs, enable the program before connection
+ android.util.Log.d("AlternativeBillingScreen", "Enabling billing program: $selectedBillingProgram")
+ iapStore.enableBillingProgram(selectedBillingProgram)
+ null // No special config needed, program is enabled separately
+ }
}
android.util.Log.d("AlternativeBillingScreen", "Reconnecting with config: $config")
@@ -255,9 +272,9 @@ fun AlternativeBillingScreen(navController: NavController) {
) {
OutlinedTextField(
value = when (selectedMode) {
- AlternativeBillingMode.ALTERNATIVE_ONLY -> "Alternative Billing Only"
- AlternativeBillingMode.USER_CHOICE -> "User Choice Billing"
- else -> "None"
+ BillingModeOption.ALTERNATIVE_ONLY -> "Alternative Billing Only (Legacy)"
+ BillingModeOption.USER_CHOICE -> "User Choice Billing (Legacy)"
+ BillingModeOption.BILLING_PROGRAMS -> "Billing Programs (8.2.0+)"
},
onValueChange = {},
readOnly = true,
@@ -275,10 +292,39 @@ fun AlternativeBillingScreen(navController: NavController) {
onDismissRequest = { isModeDropdownExpanded = false }
) {
DropdownMenuItem(
- text = { Text("Alternative Billing Only") },
+ text = {
+ Column {
+ Text("Billing Programs (8.2.0+)")
+ Text(
+ "Recommended - New API",
+ style = MaterialTheme.typography.bodySmall,
+ color = AppColors.success
+ )
+ }
+ },
+ onClick = {
+ selectedProduct = null
+ selectedMode = BillingModeOption.BILLING_PROGRAMS
+ isModeDropdownExpanded = false
+ },
+ leadingIcon = {
+ Icon(Icons.Default.Star, contentDescription = null, tint = AppColors.success)
+ }
+ )
+ DropdownMenuItem(
+ text = {
+ Column {
+ Text("Alternative Billing Only")
+ Text(
+ "Legacy 6.2+ API",
+ style = MaterialTheme.typography.bodySmall,
+ color = AppColors.textSecondary
+ )
+ }
+ },
onClick = {
selectedProduct = null
- selectedMode = AlternativeBillingMode.ALTERNATIVE_ONLY
+ selectedMode = BillingModeOption.ALTERNATIVE_ONLY
isModeDropdownExpanded = false
},
leadingIcon = {
@@ -286,10 +332,19 @@ fun AlternativeBillingScreen(navController: NavController) {
}
)
DropdownMenuItem(
- text = { Text("User Choice Billing") },
+ text = {
+ Column {
+ Text("User Choice Billing")
+ Text(
+ "Legacy 7.0+ API",
+ style = MaterialTheme.typography.bodySmall,
+ color = AppColors.textSecondary
+ )
+ }
+ },
onClick = {
selectedProduct = null
- selectedMode = AlternativeBillingMode.USER_CHOICE
+ selectedMode = BillingModeOption.USER_CHOICE
isModeDropdownExpanded = false
},
leadingIcon = {
@@ -298,6 +353,31 @@ fun AlternativeBillingScreen(navController: NavController) {
)
}
}
+
+ // Billing Program Type selector (only for BILLING_PROGRAMS mode)
+ if (selectedMode == BillingModeOption.BILLING_PROGRAMS) {
+ Spacer(modifier = Modifier.height(12.dp))
+ Text(
+ "Program Type",
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.Medium
+ )
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ modifier = Modifier.padding(top = 8.dp)
+ ) {
+ FilterChip(
+ selected = selectedBillingProgram == BillingProgramAndroid.ExternalOffer,
+ onClick = { selectedBillingProgram = BillingProgramAndroid.ExternalOffer },
+ label = { Text("External Offer") }
+ )
+ FilterChip(
+ selected = selectedBillingProgram == BillingProgramAndroid.ExternalContentLink,
+ onClick = { selectedBillingProgram = BillingProgramAndroid.ExternalContentLink },
+ label = { Text("External Content Link") }
+ )
+ }
+ }
}
}
}
@@ -320,42 +400,61 @@ fun AlternativeBillingScreen(navController: NavController) {
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
- Icons.Default.Info,
+ if (selectedMode == BillingModeOption.BILLING_PROGRAMS) Icons.Default.Star else Icons.Default.Info,
contentDescription = null,
- tint = AppColors.warning
+ tint = if (selectedMode == BillingModeOption.BILLING_PROGRAMS) AppColors.success else AppColors.warning
)
Text(
- if (selectedMode == AlternativeBillingMode.ALTERNATIVE_ONLY)
- "Alternative Billing Only"
- else
- "User Choice Billing",
+ when (selectedMode) {
+ BillingModeOption.BILLING_PROGRAMS -> "Billing Programs (8.2.0+)"
+ BillingModeOption.ALTERNATIVE_ONLY -> "Alternative Billing Only (Legacy)"
+ BillingModeOption.USER_CHOICE -> "User Choice Billing (Legacy)"
+ },
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
Text(
- if (selectedMode == AlternativeBillingMode.ALTERNATIVE_ONLY) {
- "Alternative Billing Only Mode:\n\n" +
- "• Users CANNOT use Google Play billing\n" +
- "• Only your payment system is available\n" +
- "• Requires manual 3-step flow:\n" +
- " 1. Check availability\n" +
- " 2. Show info dialog\n" +
- " 3. Process payment → Create token\n\n" +
- "• No onPurchaseUpdated callback\n" +
- "• Must report to Google within 24h"
- } else {
- "User Choice Billing Mode:\n\n" +
- "• Users CAN choose between:\n" +
- " - Google Play (30% fee)\n" +
- " - Your payment system (lower fee)\n" +
- "• Google shows selection dialog automatically\n" +
- "• If user selects Google Play:\n" +
- " → onPurchaseUpdated callback\n" +
- "• If user selects alternative:\n" +
- " → UserChoiceBillingListener callback\n" +
- " → Process payment → Report to Google"
+ when (selectedMode) {
+ BillingModeOption.BILLING_PROGRAMS -> {
+ "Billing Programs API (8.2.0+):\n\n" +
+ "✨ NEW: Recommended approach\n\n" +
+ "• Program Types:\n" +
+ " - ExternalOffer: Alternative payment\n" +
+ " - ExternalContentLink: Reader/music apps\n\n" +
+ "• Flow:\n" +
+ " 1. enableBillingProgram() before init\n" +
+ " 2. isBillingProgramAvailable() check\n" +
+ " 3. launchExternalLink() to browser\n" +
+ " 4. Process payment in your system\n" +
+ " 5. createBillingProgramReportingDetails()\n\n" +
+ "• Must report token to Google within 24h"
+ }
+ BillingModeOption.ALTERNATIVE_ONLY -> {
+ "Alternative Billing Only Mode (Legacy):\n\n" +
+ "⚠️ Deprecated in 8.2.0+\n\n" +
+ "• Users CANNOT use Google Play billing\n" +
+ "• Only your payment system is available\n" +
+ "• Requires manual 3-step flow:\n" +
+ " 1. Check availability\n" +
+ " 2. Show info dialog\n" +
+ " 3. Process payment → Create token\n\n" +
+ "• No onPurchaseUpdated callback\n" +
+ "• Must report to Google within 24h"
+ }
+ BillingModeOption.USER_CHOICE -> {
+ "User Choice Billing Mode (Legacy):\n\n" +
+ "• Users CAN choose between:\n" +
+ " - Google Play (30% fee)\n" +
+ " - Your payment system (lower fee)\n" +
+ "• Google shows selection dialog automatically\n" +
+ "• If user selects Google Play:\n" +
+ " → onPurchaseUpdated callback\n" +
+ "• If user selects alternative:\n" +
+ " → UserChoiceBillingListener callback\n" +
+ " → Process payment → Report to Google"
+ }
},
style = MaterialTheme.typography.bodySmall,
color = AppColors.textSecondary
@@ -394,7 +493,11 @@ fun AlternativeBillingScreen(navController: NavController) {
)
Text(
if (connectionStatus) {
- "Connected (${if (selectedMode == AlternativeBillingMode.ALTERNATIVE_ONLY) "Alternative Only" else "User Choice"})"
+ "Connected (${when (selectedMode) {
+ BillingModeOption.BILLING_PROGRAMS -> "Billing Programs"
+ BillingModeOption.ALTERNATIVE_ONLY -> "Alternative Only"
+ BillingModeOption.USER_CHOICE -> "User Choice"
+ }})"
} else "Disconnected",
style = MaterialTheme.typography.bodySmall,
color = AppColors.textSecondary
@@ -515,113 +618,223 @@ fun AlternativeBillingScreen(navController: NavController) {
}
// Show button based on selected mode
- if (selectedMode == AlternativeBillingMode.ALTERNATIVE_ONLY) {
- // Alternative Billing Only Button
- Button(
- onClick = {
- scope.launch {
- try {
- iapStore.setActivity(activity)
-
- // Step 1: Check availability
- val isAvailable = iapStore.checkAlternativeBillingAvailability()
- if (!isAvailable) {
- iapStore.postStatusMessage(
- "Alternative billing not available",
- PurchaseResultStatus.Error
+ when (selectedMode) {
+ BillingModeOption.BILLING_PROGRAMS -> {
+ // Billing Programs (8.2.0+) Button
+ Button(
+ onClick = {
+ scope.launch {
+ try {
+ iapStore.setActivity(activity)
+
+ // Step 1: Check availability
+ val availabilityResult = iapStore.isBillingProgramAvailable(selectedBillingProgram)
+ if (!availabilityResult.isAvailable) {
+ iapStore.postStatusMessage(
+ "Billing program not available: $selectedBillingProgram\n\nPossible causes:\n• Requires Billing Library 8.2.0+\n• Not configured in Play Console\n• Region restrictions",
+ PurchaseResultStatus.Error
+ )
+ return@launch
+ }
+
+ // Step 2: Launch external link
+ val currentActivity = activity
+ if (currentActivity == null) {
+ iapStore.postStatusMessage(
+ "Activity not available",
+ PurchaseResultStatus.Error
+ )
+ return@launch
+ }
+
+ val launched = iapStore.launchExternalLink(
+ currentActivity,
+ LaunchExternalLinkParamsAndroid(
+ billingProgram = selectedBillingProgram,
+ launchMode = ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp,
+ linkType = ExternalLinkTypeAndroid.LinkToDigitalContentOffer,
+ linkUri = "https://example.com/checkout?product=${selectedProduct!!.id}"
+ )
)
- return@launch
- }
- // Step 2: Show information dialog
- val dialogAccepted = iapStore.showAlternativeBillingInformationDialog(activity!!)
- if (!dialogAccepted) {
- iapStore.postStatusMessage(
- "User canceled",
- PurchaseResultStatus.Info
- )
- return@launch
- }
+ if (!launched) {
+ iapStore.postStatusMessage(
+ "Failed to launch external link",
+ PurchaseResultStatus.Error
+ )
+ return@launch
+ }
- // Step 2.5: Process payment (DEMO - not implemented)
- android.util.Log.d("AlternativeBilling", "⚠️ Payment processing not implemented")
+ // Step 3: Process payment (DEMO - not implemented)
+ android.util.Log.d("BillingPrograms", "⚠️ Payment processing not implemented - this is a demo")
- // Step 3: Create token
- val token = iapStore.createAlternativeBillingReportingToken()
- if (token != null) {
+ // Step 4: Create reporting details
+ val reportingDetails = iapStore.createBillingProgramReportingDetails(selectedBillingProgram)
iapStore.postStatusMessage(
- "Alternative billing completed (DEMO)\nToken: ${token.take(20)}...\n⚠️ Backend reporting required",
+ "✅ Billing Programs flow completed (DEMO)\n\n" +
+ "Program: ${reportingDetails.billingProgram}\n" +
+ "Token: ${reportingDetails.externalTransactionToken.take(20)}...\n\n" +
+ "⚠️ Next steps:\n" +
+ "1. Process payment in your system\n" +
+ "2. Report token to Google within 24h",
PurchaseResultStatus.Info,
selectedProduct!!.id
)
- } else {
+ } catch (e: Exception) {
+ android.util.Log.e("BillingPrograms", "Error: ${e.message}", e)
iapStore.postStatusMessage(
- "Failed to create reporting token",
+ "Error: ${e.message}",
PurchaseResultStatus.Error
)
}
- } catch (e: Exception) {
- // Error handled by store
}
- }
- },
- modifier = Modifier.fillMaxWidth(),
- enabled = !status.isLoading && connectionStatus,
- colors = ButtonDefaults.buttonColors(
- containerColor = AppColors.primary
- )
- ) {
- Icon(
- Icons.Default.ShoppingCart,
- contentDescription = null,
- modifier = Modifier.size(20.dp)
- )
- Spacer(Modifier.width(8.dp))
- Text("Buy (Alternative Billing Only)")
+ },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = !status.isLoading && connectionStatus,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = AppColors.success
+ )
+ ) {
+ Icon(
+ Icons.Default.Star,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(Modifier.width(8.dp))
+ Text("Buy (Billing Programs 8.2.0+)")
+ }
}
- } else {
- // User Choice Button
- Button(
- onClick = {
- scope.launch {
- try {
- iapStore.setActivity(activity)
-
- // User Choice: Just call requestPurchase
- // Google will show selection dialog automatically
- val props = RequestPurchaseProps(
- request = RequestPurchaseProps.Request.Purchase(
- RequestPurchasePropsByPlatforms(
- android = RequestPurchaseAndroidProps(
- skus = listOf(selectedProduct!!.id)
- )
+
+ BillingModeOption.ALTERNATIVE_ONLY -> {
+ // Alternative Billing Only Button (Legacy)
+ Button(
+ onClick = {
+ scope.launch {
+ try {
+ iapStore.setActivity(activity)
+
+ // Step 1: Check availability
+ @Suppress("DEPRECATION")
+ val isAvailable = iapStore.checkAlternativeBillingAvailability()
+ if (!isAvailable) {
+ iapStore.postStatusMessage(
+ "Alternative billing not available",
+ PurchaseResultStatus.Error
)
- ),
- type = ProductQueryType.InApp
- )
+ return@launch
+ }
+
+ // Step 2: Show information dialog
+ val currentActivity = activity
+ if (currentActivity == null) {
+ iapStore.postStatusMessage(
+ "Activity not available",
+ PurchaseResultStatus.Error
+ )
+ return@launch
+ }
+
+ @Suppress("DEPRECATION")
+ val dialogAccepted = iapStore.showAlternativeBillingInformationDialog(currentActivity)
+ if (!dialogAccepted) {
+ iapStore.postStatusMessage(
+ "User canceled",
+ PurchaseResultStatus.Info
+ )
+ return@launch
+ }
+
+ // Step 2.5: Process payment (DEMO - not implemented)
+ android.util.Log.d("AlternativeBilling", "⚠️ Payment processing not implemented")
+
+ // Step 3: Create token
+ @Suppress("DEPRECATION")
+ val token = iapStore.createAlternativeBillingReportingToken()
+ if (token != null) {
+ iapStore.postStatusMessage(
+ "Alternative billing completed (DEMO)\nToken: ${token.take(20)}...\n⚠️ Backend reporting required",
+ PurchaseResultStatus.Info,
+ selectedProduct!!.id
+ )
+ } else {
+ iapStore.postStatusMessage(
+ "Failed to create reporting token",
+ PurchaseResultStatus.Error
+ )
+ }
+ } catch (e: Exception) {
+ android.util.Log.e("AlternativeBilling", "Legacy alternative billing error: ${e.message}", e)
+ iapStore.postStatusMessage(
+ "Alternative billing failed: ${e.message}",
+ PurchaseResultStatus.Error
+ )
+ }
+ }
+ },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = !status.isLoading && connectionStatus,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = AppColors.primary
+ )
+ ) {
+ Icon(
+ Icons.Default.ShoppingCart,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(Modifier.width(8.dp))
+ Text("Buy (Legacy Alternative Billing)")
+ }
+ }
- iapStore.requestPurchase(props)
+ BillingModeOption.USER_CHOICE -> {
+ // User Choice Button (Legacy)
+ Button(
+ onClick = {
+ scope.launch {
+ try {
+ iapStore.setActivity(activity)
+
+ // User Choice: Just call requestPurchase
+ // Google will show selection dialog automatically
+ val props = RequestPurchaseProps(
+ request = RequestPurchaseProps.Request.Purchase(
+ RequestPurchasePropsByPlatforms(
+ android = RequestPurchaseAndroidProps(
+ skus = listOf(selectedProduct!!.id)
+ )
+ )
+ ),
+ type = ProductQueryType.InApp
+ )
- // If user selects Google Play → onPurchaseUpdated callback
- // If user selects alternative → UserChoiceBillingListener callback
- } catch (e: Exception) {
- // Error handled by store
+ iapStore.requestPurchase(props)
+
+ // If user selects Google Play → onPurchaseUpdated callback
+ // If user selects alternative → UserChoiceBillingListener callback
+ } catch (e: Exception) {
+ android.util.Log.e("AlternativeBilling", "User choice billing error: ${e.message}", e)
+ iapStore.postStatusMessage(
+ "User choice billing failed: ${e.message}",
+ PurchaseResultStatus.Error
+ )
+ }
}
- }
- },
- modifier = Modifier.fillMaxWidth(),
- enabled = !status.isLoading && connectionStatus,
- colors = ButtonDefaults.buttonColors(
- containerColor = AppColors.secondary
- )
- ) {
- Icon(
- Icons.Default.Person,
- contentDescription = null,
- modifier = Modifier.size(20.dp)
- )
- Spacer(Modifier.width(8.dp))
- Text("Buy (User Choice)")
+ },
+ modifier = Modifier.fillMaxWidth(),
+ enabled = !status.isLoading && connectionStatus,
+ colors = ButtonDefaults.buttonColors(
+ containerColor = AppColors.secondary
+ )
+ ) {
+ Icon(
+ Icons.Default.Person,
+ contentDescription = null,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(Modifier.width(8.dp))
+ Text("Buy (Legacy User Choice)")
+ }
}
}
}
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 9ba088bd..011bcb34 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
@@ -43,6 +43,8 @@ import dev.hyo.openiap.RequestSubscriptionPropsByPlatforms
import dev.hyo.openiap.AndroidSubscriptionOfferInput
import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitGoogleProps
import dev.hyo.openiap.RequestVerifyPurchaseWithIapkitProps
+import dev.hyo.openiap.SubscriptionProductReplacementParamsAndroid
+import dev.hyo.openiap.SubscriptionReplacementModeAndroid
import dev.hyo.openiap.utils.verifyPurchaseWithIapkit
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -57,12 +59,15 @@ import dev.hyo.martie.util.resolvePremiumOfferInfo
import dev.hyo.martie.util.savePremiumOffer
// Google Play Billing SubscriptionReplacementMode values
+// See: https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
private object ReplacementMode {
- const val WITHOUT_PRORATION = 1 // No proration
- const val CHARGE_PRORATED_PRICE = 2 // Charge prorated amount immediately
- const val DEFERRED = 3 // Change takes effect at next billing cycle
- const val WITH_TIME_PRORATION = 4 // Time-based proration
- const val CHARGE_FULL_PRICE = 5 // Charge full price immediately
+ const val UNKNOWN_REPLACEMENT_MODE = 0
+ const val WITH_TIME_PRORATION = 1 // Immediate change with prorated credit
+ const val CHARGE_PRORATED_PRICE = 2 // Immediate change, charge difference (upgrade only)
+ const val WITHOUT_PRORATION = 3 // Immediate change, no proration
+ const val CHARGE_FULL_PRICE = 5 // Immediate change, charge full price
+ const val DEFERRED = 6 // Change at next billing cycle
+ const val KEEP_EXISTING = 7 // Keep existing payment schedule (8.1.0+)
}
// Helper to format remaining time like "3d 4h" / "2h 12m" / "35m"
@@ -768,10 +773,8 @@ fun SubscriptionFlowScreen(
println("SubscriptionFlow [Horizon/Play]: Changing from ${currentOffer.basePlanId} to ${targetOffer.basePlanId} with token: ${purchaseToken.take(10)}...")
- // Use CHARGE_FULL_PRICE for plan changes
- val replacementMode = ReplacementMode.CHARGE_FULL_PRICE
-
// Request subscription offer change (same product, different offer)
+ // Using new subscriptionProductReplacementParams API (8.1.0+)
val offerInputs = listOf(
AndroidSubscriptionOfferInput(
sku = IapConstants.PREMIUM_PRODUCT_ID,
@@ -786,7 +789,11 @@ fun SubscriptionFlowScreen(
obfuscatedAccountIdAndroid = null,
obfuscatedProfileIdAndroid = null,
purchaseTokenAndroid = purchaseToken,
- replacementModeAndroid = replacementMode,
+ // New 8.1.0+ API: per-product replacement params
+ subscriptionProductReplacementParams = SubscriptionProductReplacementParamsAndroid(
+ oldProductId = IapConstants.PREMIUM_PRODUCT_ID,
+ replacementMode = SubscriptionReplacementModeAndroid.ChargeFullPrice
+ ),
skus = listOf(IapConstants.PREMIUM_PRODUCT_ID),
subscriptionOffers = offerInputs
)
diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
index f4e87503..13e1d488 100644
--- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
+++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
@@ -901,6 +901,7 @@ class OpenIapModule(
}
// Alternative Billing - Testing if supported by Horizon Billing Compatibility Library
+ @Deprecated("Use isBillingProgramAvailable with BillingProgramAndroid.ExternalOffer instead")
override suspend fun checkAlternativeBillingAvailability(): Boolean = withContext(Dispatchers.IO) {
try {
val client = billingClient ?: throw Exception("Not connected")
@@ -931,6 +932,7 @@ class OpenIapModule(
}
}
+ @Deprecated("Use launchExternalLink instead")
override suspend fun showAlternativeBillingInformationDialog(activity: Activity): Boolean = withContext(Dispatchers.IO) {
try {
val client = billingClient ?: throw Exception("Not connected")
@@ -968,6 +970,7 @@ class OpenIapModule(
}
}
+ @Deprecated("Use createBillingProgramReportingDetails with BillingProgramAndroid.ExternalOffer instead")
override suspend fun createAlternativeBillingReportingToken(): String? = withContext(Dispatchers.IO) {
try {
val client = billingClient ?: throw Exception("Not connected")
@@ -1011,4 +1014,23 @@ class OpenIapModule(
override fun removeUserChoiceBillingListener(listener: OpenIapUserChoiceBillingListener) {
// Not supported on Horizon
}
+
+ // Billing Programs (8.2.0+) - Not supported on Horizon
+ override suspend fun isBillingProgramAvailable(program: BillingProgramAndroid): BillingProgramAvailabilityResultAndroid {
+ OpenIapLog.w("isBillingProgramAvailable not supported on Horizon", TAG)
+ return BillingProgramAvailabilityResultAndroid(
+ billingProgram = program,
+ isAvailable = false
+ )
+ }
+
+ override suspend fun createBillingProgramReportingDetails(program: BillingProgramAndroid): BillingProgramReportingDetailsAndroid {
+ OpenIapLog.w("createBillingProgramReportingDetails not supported on Horizon", TAG)
+ throw OpenIapError.FeatureNotSupported
+ }
+
+ override suspend fun launchExternalLink(activity: Activity, params: LaunchExternalLinkParamsAndroid): Boolean {
+ OpenIapLog.w("launchExternalLink not supported on Horizon", TAG)
+ return false
+ }
}
diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt
index 93b4cdd5..b6af70ff 100644
--- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt
+++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/helpers/SharedHelpers.kt
@@ -7,6 +7,7 @@ import dev.hyo.openiap.ProductQueryType
import dev.hyo.openiap.Purchase
import dev.hyo.openiap.PurchaseError
import dev.hyo.openiap.RequestPurchaseProps
+import dev.hyo.openiap.SubscriptionProductReplacementParamsAndroid
import dev.hyo.openiap.listener.OpenIapPurchaseErrorListener
import dev.hyo.openiap.listener.OpenIapPurchaseUpdateListener
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -57,6 +58,7 @@ internal data class AndroidPurchaseArgs(
val purchaseTokenAndroid: String?,
val replacementModeAndroid: Int?,
val subscriptionOffers: List
?,
+ val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid?,
val type: ProductQueryType,
val useAlternativeBilling: Boolean?
)
@@ -77,6 +79,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs {
purchaseTokenAndroid = null,
replacementModeAndroid = null,
subscriptionOffers = null,
+ subscriptionProductReplacementParams = null,
type = type,
useAlternativeBilling = useAlternativeBilling
)
@@ -97,6 +100,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs {
purchaseTokenAndroid = android.purchaseTokenAndroid,
replacementModeAndroid = android.replacementModeAndroid,
subscriptionOffers = android.subscriptionOffers,
+ subscriptionProductReplacementParams = android.subscriptionProductReplacementParams,
type = type,
useAlternativeBilling = useAlternativeBilling
)
diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt
index 2579cd91..36c16344 100644
--- a/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt
+++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/OpenIapProtocol.kt
@@ -41,10 +41,42 @@ interface OpenIapProtocol {
fun removePurchaseErrorListener(listener: OpenIapPurchaseErrorListener)
// Alternative Billing (Google Play only)
+ @Deprecated("Use isBillingProgramAvailable with BillingProgramAndroid.ExternalOffer instead")
suspend fun checkAlternativeBillingAvailability(): Boolean
+ @Deprecated("Use launchExternalLink instead")
suspend fun showAlternativeBillingInformationDialog(activity: Activity): Boolean
+ @Deprecated("Use createBillingProgramReportingDetails with BillingProgramAndroid.ExternalOffer instead")
suspend fun createAlternativeBillingReportingToken(): String?
fun setUserChoiceBillingListener(listener: dev.hyo.openiap.listener.UserChoiceBillingListener?)
fun addUserChoiceBillingListener(listener: OpenIapUserChoiceBillingListener)
fun removeUserChoiceBillingListener(listener: OpenIapUserChoiceBillingListener)
+
+ // Billing Programs (Google Play Billing Library 8.2.0+)
+ /**
+ * Check if a billing program is available for this user/device.
+ * Replaces checkAlternativeBillingAvailability() for external offers.
+ *
+ * @param program The billing program to check (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ * @return Result containing availability information
+ */
+ suspend fun isBillingProgramAvailable(program: BillingProgramAndroid): BillingProgramAvailabilityResultAndroid
+
+ /**
+ * Create reporting details for transactions made outside of Google Play Billing.
+ * Replaces createAlternativeBillingReportingToken() for external offers.
+ *
+ * @param program The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ * @return Reporting details containing the external transaction token
+ */
+ suspend fun createBillingProgramReportingDetails(program: BillingProgramAndroid): BillingProgramReportingDetailsAndroid
+
+ /**
+ * Launch an external link for external offer or app download.
+ * Replaces showAlternativeBillingInformationDialog() for external offers.
+ *
+ * @param activity Current activity context
+ * @param params Parameters for the external link
+ * @return true if launch was successful, false otherwise
+ */
+ suspend fun launchExternalLink(activity: Activity, params: LaunchExternalLinkParamsAndroid): Boolean
}
diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
index 3139eae7..f193f38e 100644
--- a/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
+++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/Types.kt
@@ -43,6 +43,41 @@ public enum class AlternativeBillingModeAndroid(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * Billing program types for external content links and external offers (Android)
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public enum class BillingProgramAndroid(val rawValue: String) {
+ /**
+ * Unspecified billing program. Do not use.
+ */
+ Unspecified("unspecified"),
+ /**
+ * External Content Links program.
+ * Allows linking to external content outside the app.
+ */
+ ExternalContentLink("external-content-link"),
+ /**
+ * External Offers program.
+ * Allows offering digital content purchases outside the app.
+ */
+ ExternalOffer("external-offer");
+
+ companion object {
+ fun fromJson(value: String): BillingProgramAndroid = when (value) {
+ "unspecified" -> BillingProgramAndroid.Unspecified
+ "Unspecified" -> BillingProgramAndroid.Unspecified
+ "external-content-link" -> BillingProgramAndroid.ExternalContentLink
+ "ExternalContentLink" -> BillingProgramAndroid.ExternalContentLink
+ "external-offer" -> BillingProgramAndroid.ExternalOffer
+ "ExternalOffer" -> BillingProgramAndroid.ExternalOffer
+ else -> throw IllegalArgumentException("Unknown BillingProgramAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class ErrorCode(val rawValue: String) {
Unknown("unknown"),
UserCancelled("user-cancelled"),
@@ -165,6 +200,74 @@ public enum class ErrorCode(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * Launch mode for external link flow (Android)
+ * Determines how the external URL is launched
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public enum class ExternalLinkLaunchModeAndroid(val rawValue: String) {
+ /**
+ * Unspecified launch mode. Do not use.
+ */
+ Unspecified("unspecified"),
+ /**
+ * Play will launch the URL in an external browser or eligible app
+ */
+ LaunchInExternalBrowserOrApp("launch-in-external-browser-or-app"),
+ /**
+ * Play will not launch the URL. The app handles launching the URL after Play returns control.
+ */
+ CallerWillLaunchLink("caller-will-launch-link");
+
+ companion object {
+ fun fromJson(value: String): ExternalLinkLaunchModeAndroid = when (value) {
+ "unspecified" -> ExternalLinkLaunchModeAndroid.Unspecified
+ "UNSPECIFIED" -> ExternalLinkLaunchModeAndroid.Unspecified
+ "launch-in-external-browser-or-app" -> ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp
+ "LAUNCH_IN_EXTERNAL_BROWSER_OR_APP" -> ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp
+ "caller-will-launch-link" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink
+ "CALLER_WILL_LAUNCH_LINK" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink
+ else -> throw IllegalArgumentException("Unknown ExternalLinkLaunchModeAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
+/**
+ * Link type for external link flow (Android)
+ * Specifies the type of external link destination
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public enum class ExternalLinkTypeAndroid(val rawValue: String) {
+ /**
+ * Unspecified link type. Do not use.
+ */
+ Unspecified("unspecified"),
+ /**
+ * The link will direct users to a digital content offer
+ */
+ LinkToDigitalContentOffer("link-to-digital-content-offer"),
+ /**
+ * The link will direct users to download an app
+ */
+ LinkToAppDownload("link-to-app-download");
+
+ companion object {
+ fun fromJson(value: String): ExternalLinkTypeAndroid = when (value) {
+ "unspecified" -> ExternalLinkTypeAndroid.Unspecified
+ "Unspecified" -> ExternalLinkTypeAndroid.Unspecified
+ "link-to-digital-content-offer" -> ExternalLinkTypeAndroid.LinkToDigitalContentOffer
+ "LinkToDigitalContentOffer" -> ExternalLinkTypeAndroid.LinkToDigitalContentOffer
+ "link-to-app-download" -> ExternalLinkTypeAndroid.LinkToAppDownload
+ "LinkToAppDownload" -> ExternalLinkTypeAndroid.LinkToAppDownload
+ else -> throw IllegalArgumentException("Unknown ExternalLinkTypeAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
/**
* User actions on external purchase notice sheet (iOS 18.2+)
*/
@@ -181,10 +284,8 @@ public enum class ExternalPurchaseNoticeAction(val rawValue: String) {
companion object {
fun fromJson(value: String): ExternalPurchaseNoticeAction = when (value) {
"continue" -> ExternalPurchaseNoticeAction.Continue
- "CONTINUE" -> ExternalPurchaseNoticeAction.Continue
"Continue" -> ExternalPurchaseNoticeAction.Continue
"dismissed" -> ExternalPurchaseNoticeAction.Dismissed
- "DISMISSED" -> ExternalPurchaseNoticeAction.Dismissed
"Dismissed" -> ExternalPurchaseNoticeAction.Dismissed
else -> throw IllegalArgumentException("Unknown ExternalPurchaseNoticeAction value: $value")
}
@@ -202,16 +303,13 @@ public enum class IapEvent(val rawValue: String) {
companion object {
fun fromJson(value: String): IapEvent = when (value) {
"purchase-updated" -> IapEvent.PurchaseUpdated
- "PURCHASE_UPDATED" -> IapEvent.PurchaseUpdated
"PurchaseUpdated" -> IapEvent.PurchaseUpdated
"purchase-error" -> IapEvent.PurchaseError
- "PURCHASE_ERROR" -> IapEvent.PurchaseError
"PurchaseError" -> IapEvent.PurchaseError
"promoted-product-ios" -> IapEvent.PromotedProductIos
- "PROMOTED_PRODUCT_IOS" -> IapEvent.PromotedProductIos
+ "PromotedProductIos" -> IapEvent.PromotedProductIos
"PromotedProductIOS" -> IapEvent.PromotedProductIos
"user-choice-billing-android" -> IapEvent.UserChoiceBillingAndroid
- "USER_CHOICE_BILLING_ANDROID" -> IapEvent.UserChoiceBillingAndroid
"UserChoiceBillingAndroid" -> IapEvent.UserChoiceBillingAndroid
else -> throw IllegalArgumentException("Unknown IapEvent value: $value")
}
@@ -498,6 +596,64 @@ public enum class SubscriptionPeriodIOS(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * Replacement mode for subscription changes (Android)
+ * These modes determine how the subscription replacement affects billing.
+ * Available in Google Play Billing Library 8.1.0+
+ */
+public enum class SubscriptionReplacementModeAndroid(val rawValue: String) {
+ /**
+ * Unknown replacement mode. Do not use.
+ */
+ UnknownReplacementMode("unknown-replacement-mode"),
+ /**
+ * Replacement takes effect immediately, and the new expiration time will be prorated.
+ */
+ WithTimeProration("with-time-proration"),
+ /**
+ * Replacement takes effect immediately, and the billing cycle remains the same.
+ */
+ ChargeProratedPrice("charge-prorated-price"),
+ /**
+ * Replacement takes effect immediately, and the user is charged full price immediately.
+ */
+ ChargeFullPrice("charge-full-price"),
+ /**
+ * Replacement takes effect when the old plan expires.
+ */
+ WithoutProration("without-proration"),
+ /**
+ * Replacement takes effect when the old plan expires, and the user is not charged.
+ */
+ Deferred("deferred"),
+ /**
+ * Keep the existing payment schedule unchanged for the item (8.1.0+)
+ */
+ KeepExisting("keep-existing");
+
+ companion object {
+ fun fromJson(value: String): SubscriptionReplacementModeAndroid = when (value) {
+ "unknown-replacement-mode" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode
+ "UnknownReplacementMode" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode
+ "with-time-proration" -> SubscriptionReplacementModeAndroid.WithTimeProration
+ "WithTimeProration" -> SubscriptionReplacementModeAndroid.WithTimeProration
+ "charge-prorated-price" -> SubscriptionReplacementModeAndroid.ChargeProratedPrice
+ "ChargeProratedPrice" -> SubscriptionReplacementModeAndroid.ChargeProratedPrice
+ "charge-full-price" -> SubscriptionReplacementModeAndroid.ChargeFullPrice
+ "ChargeFullPrice" -> SubscriptionReplacementModeAndroid.ChargeFullPrice
+ "without-proration" -> SubscriptionReplacementModeAndroid.WithoutProration
+ "WithoutProration" -> SubscriptionReplacementModeAndroid.WithoutProration
+ "deferred" -> SubscriptionReplacementModeAndroid.Deferred
+ "Deferred" -> SubscriptionReplacementModeAndroid.Deferred
+ "keep-existing" -> SubscriptionReplacementModeAndroid.KeepExisting
+ "KeepExisting" -> SubscriptionReplacementModeAndroid.KeepExisting
+ else -> throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
// MARK: - Interfaces
public interface ProductCommon {
@@ -670,6 +826,70 @@ public data class AppTransaction(
)
}
+/**
+ * Result of checking billing program availability (Android)
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public data class BillingProgramAvailabilityResultAndroid(
+ /**
+ * The billing program that was checked
+ */
+ val billingProgram: BillingProgramAndroid,
+ /**
+ * Whether the billing program is available for the user
+ */
+ val isAvailable: Boolean
+) {
+
+ companion object {
+ fun fromJson(json: Map): BillingProgramAvailabilityResultAndroid {
+ return BillingProgramAvailabilityResultAndroid(
+ billingProgram = BillingProgramAndroid.fromJson(json["billingProgram"] as String),
+ isAvailable = json["isAvailable"] as Boolean,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "BillingProgramAvailabilityResultAndroid",
+ "billingProgram" to billingProgram.toJson(),
+ "isAvailable" to isAvailable,
+ )
+}
+
+/**
+ * Reporting details for transactions made outside of Google Play Billing (Android)
+ * Contains the external transaction token needed for reporting
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public data class BillingProgramReportingDetailsAndroid(
+ /**
+ * The billing program that the reporting details are associated with
+ */
+ val billingProgram: BillingProgramAndroid,
+ /**
+ * External transaction token used to report transactions made outside of Google Play Billing.
+ * This token must be used when reporting the external transaction to Google.
+ */
+ val externalTransactionToken: String
+) {
+
+ companion object {
+ fun fromJson(json: Map): BillingProgramReportingDetailsAndroid {
+ return BillingProgramReportingDetailsAndroid(
+ billingProgram = BillingProgramAndroid.fromJson(json["billingProgram"] as String),
+ externalTransactionToken = json["externalTransactionToken"] as String,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "BillingProgramReportingDetailsAndroid",
+ "billingProgram" to billingProgram.toJson(),
+ "externalTransactionToken" to externalTransactionToken,
+ )
+}
+
/**
* Discount amount details for one-time purchase offers (Android)
* Available in Google Play Billing Library 7.0+
@@ -842,6 +1062,58 @@ public data class EntitlementIOS(
)
}
+/**
+ * External offer availability result (Android)
+ * @deprecated Use BillingProgramAvailabilityResultAndroid with isBillingProgramAvailableAsync instead
+ * Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+ */
+public data class ExternalOfferAvailabilityResultAndroid(
+ /**
+ * Whether external offers are available for the user
+ */
+ val isAvailable: Boolean
+) {
+
+ companion object {
+ fun fromJson(json: Map): ExternalOfferAvailabilityResultAndroid {
+ return ExternalOfferAvailabilityResultAndroid(
+ isAvailable = json["isAvailable"] as Boolean,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "ExternalOfferAvailabilityResultAndroid",
+ "isAvailable" to isAvailable,
+ )
+}
+
+/**
+ * External offer reporting details (Android)
+ * @deprecated Use BillingProgramReportingDetailsAndroid with createBillingProgramReportingDetailsAsync instead
+ * Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+ */
+public data class ExternalOfferReportingDetailsAndroid(
+ /**
+ * External transaction token for reporting external offer transactions
+ */
+ val externalTransactionToken: String
+) {
+
+ companion object {
+ fun fromJson(json: Map): ExternalOfferReportingDetailsAndroid {
+ return ExternalOfferReportingDetailsAndroid(
+ externalTransactionToken = json["externalTransactionToken"] as String,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "ExternalOfferReportingDetailsAndroid",
+ "externalTransactionToken" to externalTransactionToken,
+ )
+}
+
/**
* Result of presenting an external purchase link (iOS 18.2+)
*/
@@ -2274,6 +2546,48 @@ public data class InitConnectionConfig(
)
}
+/**
+ * Parameters for launching an external link (Android)
+ * Used with launchExternalLink to initiate external offer or app install flows
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public data class LaunchExternalLinkParamsAndroid(
+ /**
+ * The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ */
+ val billingProgram: BillingProgramAndroid,
+ /**
+ * The external link launch mode
+ */
+ val launchMode: ExternalLinkLaunchModeAndroid,
+ /**
+ * The type of the external link
+ */
+ val linkType: ExternalLinkTypeAndroid,
+ /**
+ * The URI where the content will be accessed from
+ */
+ val linkUri: String
+) {
+ companion object {
+ fun fromJson(json: Map): LaunchExternalLinkParamsAndroid {
+ return LaunchExternalLinkParamsAndroid(
+ billingProgram = BillingProgramAndroid.fromJson(json["billingProgram"] as String),
+ launchMode = ExternalLinkLaunchModeAndroid.fromJson(json["launchMode"] as String),
+ linkType = ExternalLinkTypeAndroid.fromJson(json["linkType"] as String),
+ linkUri = json["linkUri"] as String,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "billingProgram" to billingProgram.toJson(),
+ "launchMode" to launchMode.toJson(),
+ "linkType" to linkType.toJson(),
+ "linkUri" to linkUri,
+ )
+}
+
public data class ProductRequest(
val skus: List,
val type: ProductQueryType? = null
@@ -2509,6 +2823,7 @@ public data class RequestSubscriptionAndroidProps(
val purchaseTokenAndroid: String? = null,
/**
* Replacement mode for subscription changes
+ * @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+)
*/
val replacementModeAndroid: Int? = null,
/**
@@ -2518,7 +2833,12 @@ public data class RequestSubscriptionAndroidProps(
/**
* Subscription offers
*/
- val subscriptionOffers: List? = null
+ val subscriptionOffers: List? = null,
+ /**
+ * Product-level replacement parameters (8.1.0+)
+ * Use this instead of replacementModeAndroid for item-level replacement
+ */
+ val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = null
) {
companion object {
fun fromJson(json: Map): RequestSubscriptionAndroidProps {
@@ -2530,6 +2850,7 @@ public data class RequestSubscriptionAndroidProps(
replacementModeAndroid = (json["replacementModeAndroid"] as Number?)?.toInt(),
skus = (json["skus"] as List<*>).map { it as String },
subscriptionOffers = (json["subscriptionOffers"] as List<*>?)?.map { AndroidSubscriptionOfferInput.fromJson((it as Map)) },
+ subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as Map?)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) },
)
}
}
@@ -2542,6 +2863,7 @@ public data class RequestSubscriptionAndroidProps(
"replacementModeAndroid" to replacementModeAndroid,
"skus" to skus.map { it },
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
+ "subscriptionProductReplacementParams" to subscriptionProductReplacementParams?.toJson(),
)
}
@@ -2679,6 +3001,36 @@ public data class RequestVerifyPurchaseWithIapkitProps(
)
}
+/**
+ * Product-level subscription replacement parameters (Android)
+ * Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
+ * Available in Google Play Billing Library 8.1.0+
+ */
+public data class SubscriptionProductReplacementParamsAndroid(
+ /**
+ * The old product ID that needs to be replaced
+ */
+ val oldProductId: String,
+ /**
+ * The replacement mode for this product change
+ */
+ val replacementMode: SubscriptionReplacementModeAndroid
+) {
+ companion object {
+ fun fromJson(json: Map): SubscriptionProductReplacementParamsAndroid {
+ return SubscriptionProductReplacementParamsAndroid(
+ oldProductId = json["oldProductId"] as String,
+ replacementMode = SubscriptionReplacementModeAndroid.fromJson(json["replacementMode"] as String),
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "oldProductId" to oldProductId,
+ "replacementMode" to replacementMode.toJson(),
+ )
+}
+
public data class VerifyPurchaseAndroidOptions(
val accessToken: String,
val isSub: Boolean? = null,
diff --git a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt
index 807eb11d..67f52341 100644
--- a/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt
+++ b/packages/google/openiap/src/main/java/dev/hyo/openiap/store/OpenIapStore.kt
@@ -33,6 +33,10 @@ import dev.hyo.openiap.QueryGetAvailablePurchasesHandler
import dev.hyo.openiap.MutationFinishTransactionHandler
import dev.hyo.openiap.MutationInitConnectionHandler
import dev.hyo.openiap.MutationEndConnectionHandler
+import dev.hyo.openiap.BillingProgramAndroid
+import dev.hyo.openiap.BillingProgramAvailabilityResultAndroid
+import dev.hyo.openiap.BillingProgramReportingDetailsAndroid
+import dev.hyo.openiap.LaunchExternalLinkParamsAndroid
import android.app.Activity
import android.content.Context
import dev.hyo.openiap.OpenIapError
@@ -419,10 +423,66 @@ class OpenIapStore(private val module: OpenIapProtocol) {
* Must be called AFTER successful payment in your payment system
* Token must be reported to Google Play backend within 24 hours
* @return External transaction token, or null if failed
+ * @deprecated Use createBillingProgramReportingDetails with BillingProgramAndroid.ExternalOffer instead
*/
+ @Deprecated("Use createBillingProgramReportingDetails with BillingProgramAndroid.ExternalOffer instead")
suspend fun createAlternativeBillingReportingToken(): String? =
module.createAlternativeBillingReportingToken()
+ // -------------------------------------------------------------------------
+ // Billing Programs (Google Play Billing Library 8.2.0+)
+ // -------------------------------------------------------------------------
+ /**
+ * Check if a billing program is available for this user/device.
+ * This is the new API that replaces checkAlternativeBillingAvailability for external offers.
+ *
+ * @param program The billing program to check (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ * @return Result containing availability information
+ */
+ suspend fun isBillingProgramAvailable(program: BillingProgramAndroid): BillingProgramAvailabilityResultAndroid =
+ module.isBillingProgramAvailable(program)
+
+ /**
+ * Create reporting details for transactions made outside of Google Play Billing.
+ * This is the new API that replaces createAlternativeBillingReportingToken for external offers.
+ *
+ * @param program The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ * @return Reporting details containing the external transaction token
+ */
+ suspend fun createBillingProgramReportingDetails(program: BillingProgramAndroid): BillingProgramReportingDetailsAndroid =
+ module.createBillingProgramReportingDetails(program)
+
+ /**
+ * Launch an external link for external offer or app download.
+ * This is the new API that replaces showAlternativeBillingInformationDialog for external offers.
+ *
+ * @param activity Current activity context
+ * @param params Parameters for the external link
+ * @return true if launch was successful, false otherwise
+ */
+ suspend fun launchExternalLink(activity: Activity, params: LaunchExternalLinkParamsAndroid): Boolean =
+ module.launchExternalLink(activity, params)
+
+ /**
+ * Enable a billing program for external content links or external offers (8.2.0+).
+ * This should be called BEFORE initConnection to configure the BillingClient.
+ *
+ * @param program The billing program to enable (ExternalOffer or ExternalContentLink)
+ */
+ fun enableBillingProgram(program: BillingProgramAndroid) {
+ // Use reflection to call enableBillingProgram on the module
+ // This is needed because the method is only available in the Play flavor
+ try {
+ val method = module.javaClass.getMethod("enableBillingProgram", BillingProgramAndroid::class.java)
+ method.invoke(module, program)
+ OpenIapLog.d("Billing program enabled via store: $program", "OpenIapStore")
+ } catch (e: NoSuchMethodException) {
+ OpenIapLog.w("enableBillingProgram not available (Horizon flavor or older library)", "OpenIapStore")
+ } catch (e: Exception) {
+ OpenIapLog.e("Failed to enable billing program: ${e.message}", e, "OpenIapStore")
+ }
+ }
+
// -------------------------------------------------------------------------
// Event listeners passthrough
// -------------------------------------------------------------------------
diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
index 3b07638a..3546f875 100644
--- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
+++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
@@ -68,6 +68,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
import java.lang.ref.WeakReference
// AlternativeBillingMode moved to main source set (shared between Play and Horizon)
@@ -107,6 +108,9 @@ class OpenIapModule(
private val userChoiceBillingListeners = mutableSetOf()
private var currentPurchaseCallback: ((Result>) -> Unit)? = null
+ // Billing programs enabled via enableBillingProgram (8.2.0+)
+ private val enabledBillingPrograms = mutableSetOf()
+
override val initConnection: MutationInitConnectionHandler = { config ->
// Update alternativeBillingMode if provided in config
config?.alternativeBillingModeAndroid?.let { modeAndroid ->
@@ -266,7 +270,9 @@ class OpenIapModule(
/**
* Check if alternative billing is available for this user/device
* Step 1 of alternative billing flow
+ * @deprecated Use isBillingProgramAvailable with BillingProgramAndroid.ExternalOffer instead
*/
+ @Deprecated("Use isBillingProgramAvailable with BillingProgramAndroid.ExternalOffer instead")
override suspend fun checkAlternativeBillingAvailability(): Boolean = withContext(Dispatchers.IO) {
val client = billingClient ?: throw OpenIapError.NotPrepared
if (!client.isReady) throw OpenIapError.NotPrepared
@@ -305,7 +311,9 @@ class OpenIapModule(
* Show alternative billing information dialog to user
* Step 2 of alternative billing flow
* Must be called BEFORE processing payment
+ * @deprecated Use launchExternalLink instead
*/
+ @Deprecated("Use launchExternalLink instead")
override suspend fun showAlternativeBillingInformationDialog(activity: Activity): Boolean = withContext(Dispatchers.IO) {
val client = billingClient ?: throw OpenIapError.NotPrepared
if (!client.isReady) throw OpenIapError.NotPrepared
@@ -353,7 +361,9 @@ class OpenIapModule(
* Step 3 of alternative billing flow
* Must be called AFTER successful payment in your payment system
* Token must be reported to Google Play backend within 24 hours
+ * @deprecated Use createBillingProgramReportingDetails with BillingProgramAndroid.ExternalOffer instead
*/
+ @Deprecated("Use createBillingProgramReportingDetails with BillingProgramAndroid.ExternalOffer instead")
override suspend fun createAlternativeBillingReportingToken(): String? = withContext(Dispatchers.IO) {
val client = billingClient ?: throw OpenIapError.NotPrepared
if (!client.isReady) throw OpenIapError.NotPrepared
@@ -395,6 +405,256 @@ class OpenIapModule(
}
}
+ /**
+ * Check if a billing program is available for this user/device (8.2.0+)
+ * This is the new API that replaces checkAlternativeBillingAvailability for external offers.
+ *
+ * @param program The billing program to check (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ * @return Result containing availability information
+ */
+ override suspend fun isBillingProgramAvailable(program: BillingProgramAndroid): BillingProgramAvailabilityResultAndroid = withContext(Dispatchers.IO) {
+ val client = billingClient ?: throw OpenIapError.NotPrepared
+ if (!client.isReady) throw OpenIapError.NotPrepared
+
+ OpenIapLog.d("Checking billing program availability for: $program", TAG)
+
+ // Convert our enum to BillingClient.BillingProgram constant
+ val billingProgramConstant = when (program) {
+ BillingProgramAndroid.ExternalContentLink -> 1 // EXTERNAL_CONTENT_LINK
+ BillingProgramAndroid.ExternalOffer -> 3 // EXTERNAL_OFFER
+ BillingProgramAndroid.Unspecified -> throw IllegalArgumentException("Cannot check availability for UNSPECIFIED program")
+ }
+
+ suspendCancellableCoroutine { continuation ->
+ try {
+ // Use reflection to call isBillingProgramAvailableAsync (8.2.0+)
+ val listenerClass = Class.forName("com.android.billingclient.api.BillingProgramAvailabilityListener")
+ val listener = java.lang.reflect.Proxy.newProxyInstance(
+ listenerClass.classLoader,
+ arrayOf(listenerClass)
+ ) { _, method, args ->
+ if (method.name == "onBillingProgramAvailabilityResponse") {
+ val result = args?.get(0) as? BillingResult
+ OpenIapLog.d("Billing program availability result: ${result?.responseCode} - ${result?.debugMessage}", TAG)
+
+ val isAvailable = result?.responseCode == BillingClient.BillingResponseCode.OK
+ if (continuation.isActive) {
+ continuation.resume(BillingProgramAvailabilityResultAndroid(
+ billingProgram = program,
+ isAvailable = isAvailable
+ ))
+ }
+ }
+ null
+ }
+
+ val method = client.javaClass.getMethod(
+ "isBillingProgramAvailableAsync",
+ Int::class.javaPrimitiveType,
+ listenerClass
+ )
+ method.invoke(client, billingProgramConstant, listener)
+ } catch (e: NoSuchMethodException) {
+ OpenIapLog.e("isBillingProgramAvailableAsync not found. Requires Billing Library 8.2.0+", e, TAG)
+ if (continuation.isActive) {
+ continuation.resume(BillingProgramAvailabilityResultAndroid(
+ billingProgram = program,
+ isAvailable = false
+ ))
+ }
+ } catch (e: Exception) {
+ OpenIapLog.e("Failed to check billing program availability: ${e.message}", e, TAG)
+ if (continuation.isActive) {
+ continuation.resume(BillingProgramAvailabilityResultAndroid(
+ billingProgram = program,
+ isAvailable = false
+ ))
+ }
+ }
+ }
+ }
+
+ /**
+ * Create reporting details for transactions made outside of Google Play Billing (8.2.0+)
+ * This is the new API that replaces createAlternativeBillingReportingToken for external offers.
+ *
+ * @param program The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ * @return Reporting details containing the external transaction token
+ */
+ override suspend fun createBillingProgramReportingDetails(program: BillingProgramAndroid): BillingProgramReportingDetailsAndroid = withContext(Dispatchers.IO) {
+ val client = billingClient ?: throw OpenIapError.NotPrepared
+ if (!client.isReady) throw OpenIapError.NotPrepared
+
+ OpenIapLog.d("Creating billing program reporting details for: $program", TAG)
+
+ val billingProgramConstant = when (program) {
+ BillingProgramAndroid.ExternalContentLink -> 1
+ BillingProgramAndroid.ExternalOffer -> 3
+ BillingProgramAndroid.Unspecified -> throw IllegalArgumentException("Cannot create reporting details for UNSPECIFIED program")
+ }
+
+ suspendCancellableCoroutine { continuation ->
+ try {
+ val listenerClass = Class.forName("com.android.billingclient.api.BillingProgramReportingDetailsListener")
+ val listener = java.lang.reflect.Proxy.newProxyInstance(
+ listenerClass.classLoader,
+ arrayOf(listenerClass)
+ ) { _, method, args ->
+ if (method.name == "onBillingProgramReportingDetailsResponse") {
+ val result = args?.get(0) as? BillingResult
+ val details = args?.getOrNull(1)
+
+ if (result?.responseCode == BillingClient.BillingResponseCode.OK && details != null) {
+ try {
+ val tokenMethod = details.javaClass.getMethod("getExternalTransactionToken")
+ val token = tokenMethod.invoke(details) as? String
+ OpenIapLog.d("Billing program reporting token created: $token", TAG)
+
+ if (continuation.isActive && token != null) {
+ continuation.resume(BillingProgramReportingDetailsAndroid(
+ billingProgram = program,
+ externalTransactionToken = token
+ ))
+ } else if (continuation.isActive) {
+ continuation.resumeWithException(OpenIapError.PurchaseFailed)
+ }
+ } catch (e: Exception) {
+ OpenIapLog.e("Failed to extract token: ${e.message}", e, TAG)
+ if (continuation.isActive) continuation.resumeWithException(OpenIapError.PurchaseFailed)
+ }
+ } else {
+ OpenIapLog.e("Reporting details creation failed: ${result?.debugMessage}", tag = TAG)
+ if (continuation.isActive) continuation.resumeWithException(OpenIapError.PurchaseFailed)
+ }
+ }
+ null
+ }
+
+ val method = client.javaClass.getMethod(
+ "createBillingProgramReportingDetailsAsync",
+ Int::class.javaPrimitiveType,
+ listenerClass
+ )
+ method.invoke(client, billingProgramConstant, listener)
+ } catch (e: NoSuchMethodException) {
+ OpenIapLog.e("createBillingProgramReportingDetailsAsync not found. Requires Billing Library 8.2.0+", e, TAG)
+ throw OpenIapError.FeatureNotSupported
+ } catch (e: Exception) {
+ OpenIapLog.e("Failed to create billing program reporting details: ${e.message}", e, TAG)
+ throw OpenIapError.PurchaseFailed
+ }
+ }
+ }
+
+ /**
+ * Launch an external link for external offer or app download (8.2.0+)
+ * This is the new API that replaces showExternalOfferInformationDialog.
+ *
+ * @param activity Current activity context
+ * @param params Parameters for the external link
+ * @return true if launch was successful, false otherwise
+ */
+ override suspend fun launchExternalLink(activity: Activity, params: LaunchExternalLinkParamsAndroid): Boolean = withContext(Dispatchers.IO) {
+ val client = billingClient ?: throw OpenIapError.NotPrepared
+ if (!client.isReady) throw OpenIapError.NotPrepared
+
+ OpenIapLog.d("Launching external link: program=${params.billingProgram}, launchMode=${params.launchMode}, linkType=${params.linkType}", TAG)
+
+ // Convert enums to BillingClient constants
+ val billingProgramConstant = when (params.billingProgram) {
+ BillingProgramAndroid.ExternalContentLink -> 1
+ BillingProgramAndroid.ExternalOffer -> 3
+ BillingProgramAndroid.Unspecified -> throw IllegalArgumentException("Cannot launch with UNSPECIFIED program")
+ }
+
+ val launchModeConstant = when (params.launchMode) {
+ ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp -> 1
+ ExternalLinkLaunchModeAndroid.CallerWillLaunchLink -> 2
+ ExternalLinkLaunchModeAndroid.Unspecified -> throw IllegalArgumentException("Cannot launch with UNSPECIFIED launch mode")
+ }
+
+ val linkTypeConstant = when (params.linkType) {
+ ExternalLinkTypeAndroid.LinkToDigitalContentOffer -> 1
+ ExternalLinkTypeAndroid.LinkToAppDownload -> 2
+ ExternalLinkTypeAndroid.Unspecified -> throw IllegalArgumentException("Cannot launch with UNSPECIFIED link type")
+ }
+
+ suspendCancellableCoroutine { continuation ->
+ try {
+ // Build LaunchExternalLinkParams using reflection
+ val paramsClass = Class.forName("com.android.billingclient.api.LaunchExternalLinkParams")
+ val builderClass = Class.forName("com.android.billingclient.api.LaunchExternalLinkParams\$Builder")
+
+ val newBuilderMethod = paramsClass.getMethod("newBuilder")
+ val builder = newBuilderMethod.invoke(null)
+
+ // Set billing program
+ val setBillingProgramMethod = builderClass.getMethod("setBillingProgram", Int::class.javaPrimitiveType)
+ setBillingProgramMethod.invoke(builder, billingProgramConstant)
+
+ // Set launch mode
+ val setLaunchModeMethod = builderClass.getMethod("setLaunchMode", Int::class.javaPrimitiveType)
+ setLaunchModeMethod.invoke(builder, launchModeConstant)
+
+ // Set link type
+ val setLinkTypeMethod = builderClass.getMethod("setLinkType", Int::class.javaPrimitiveType)
+ setLinkTypeMethod.invoke(builder, linkTypeConstant)
+
+ // Set link URI
+ val setLinkUriMethod = builderClass.getMethod("setLinkUri", android.net.Uri::class.java)
+ setLinkUriMethod.invoke(builder, android.net.Uri.parse(params.linkUri))
+
+ // Build the params
+ val buildMethod = builderClass.getMethod("build")
+ val launchParams = buildMethod.invoke(builder)
+
+ // Create the response listener
+ val listenerClass = Class.forName("com.android.billingclient.api.LaunchExternalLinkResponseListener")
+ val listener = java.lang.reflect.Proxy.newProxyInstance(
+ listenerClass.classLoader,
+ arrayOf(listenerClass)
+ ) { _, method, args ->
+ if (method.name == "onLaunchExternalLinkResponse") {
+ val result = args?.get(0) as? BillingResult
+ OpenIapLog.d("External link launch result: ${result?.responseCode} - ${result?.debugMessage}", TAG)
+
+ val success = result?.responseCode == BillingClient.BillingResponseCode.OK
+ if (continuation.isActive) continuation.resume(success)
+ }
+ null
+ }
+
+ // Call launchExternalLink
+ val launchMethod = client.javaClass.getMethod(
+ "launchExternalLink",
+ android.app.Activity::class.java,
+ paramsClass,
+ listenerClass
+ )
+ launchMethod.invoke(client, activity, launchParams, listener)
+ } catch (e: NoSuchMethodException) {
+ OpenIapLog.e("launchExternalLink not found. Requires Billing Library 8.2.0+", e, TAG)
+ if (continuation.isActive) continuation.resume(false)
+ } catch (e: Exception) {
+ OpenIapLog.e("Failed to launch external link: ${e.message}", e, TAG)
+ if (continuation.isActive) continuation.resume(false)
+ }
+ }
+ }
+
+ /**
+ * Enable a billing program for external content links or external offers (8.2.0+)
+ * This should be called before initConnection to configure the BillingClient.
+ *
+ * @param program The billing program to enable
+ */
+ fun enableBillingProgram(program: BillingProgramAndroid) {
+ if (program != BillingProgramAndroid.Unspecified) {
+ enabledBillingPrograms.add(program)
+ OpenIapLog.d("Billing program enabled: $program", TAG)
+ }
+ }
+
override val requestPurchase: MutationRequestPurchaseHandler = { props ->
val purchases = withContext(Dispatchers.IO) {
// ALTERNATIVE_ONLY mode: Show information dialog and create token
@@ -417,6 +677,8 @@ class OpenIapModule(
try {
// Step 1: Check if alternative billing is available
+ // Using deprecated API for backward compatibility in ALTERNATIVE_ONLY mode
+ @Suppress("DEPRECATION")
val isAvailable = checkAlternativeBillingAvailability()
if (!isAvailable) {
OpenIapLog.e("Alternative billing is not available for this user/app", tag = TAG)
@@ -441,6 +703,8 @@ class OpenIapModule(
}
// Step 2: Show alternative billing information dialog
+ // Using deprecated API for backward compatibility in ALTERNATIVE_ONLY mode
+ @Suppress("DEPRECATION")
val dialogSuccess = showAlternativeBillingInformationDialog(activity)
if (!dialogSuccess) {
val err = OpenIapError.UserCancelled
@@ -464,6 +728,8 @@ class OpenIapModule(
// - YOUR_PAYMENT_SYSTEM.processPayment()
// - createAlternativeBillingReportingToken()
// ============================================================
+ // Using deprecated API for backward compatibility in ALTERNATIVE_ONLY mode
+ @Suppress("DEPRECATION")
val tokenResult = createAlternativeBillingReportingToken()
if (tokenResult != null) {
@@ -589,6 +855,15 @@ class OpenIapModule(
}
builder.setOfferToken(resolved)
+
+ // Apply per-product subscription replacement params (8.1.0+)
+ androidArgs.subscriptionProductReplacementParams?.let { replacementParams ->
+ if (replacementParams.oldProductId == productDetails.productId ||
+ androidArgs.skus.size == 1) {
+ // Apply to this product if it matches or if it's a single-product upgrade
+ applySubscriptionProductReplacementParams(builder, replacementParams)
+ }
+ }
}
paramsList += builder.build()
@@ -859,9 +1134,9 @@ class OpenIapModule(
purchaseUpdated = purchaseUpdated
)
- init {
- buildBillingClient()
- }
+ // BillingClient is built lazily in initBillingClient() so that
+ // alternativeBillingMode and enabledBillingPrograms can be configured
+ // before the first client instance is created.
suspend fun getStorefront() = withContext(Dispatchers.IO) {
val client = billingClient ?: return@withContext ""
@@ -1099,6 +1374,28 @@ class OpenIapModule(
}
}
+ // Enable billing programs (8.2.0+) for external content links and external offers
+ if (enabledBillingPrograms.isNotEmpty()) {
+ OpenIapLog.d("=== BILLING PROGRAMS INITIALIZATION (8.2.0+) ===", TAG)
+ for (program in enabledBillingPrograms) {
+ val programConstant = when (program) {
+ BillingProgramAndroid.ExternalContentLink -> 1
+ BillingProgramAndroid.ExternalOffer -> 3
+ BillingProgramAndroid.Unspecified -> continue
+ }
+ try {
+ val method = builder.javaClass.getMethod("enableBillingProgram", Int::class.javaPrimitiveType)
+ method.invoke(builder, programConstant)
+ OpenIapLog.d("✓ Billing program enabled: $program (constant=$programConstant)", TAG)
+ } catch (e: NoSuchMethodException) {
+ OpenIapLog.w("✗ enableBillingProgram not found. Requires Billing Library 8.2.0+", TAG)
+ } catch (e: Exception) {
+ OpenIapLog.w("✗ Failed to enable billing program $program: ${e.message}", TAG)
+ }
+ }
+ OpenIapLog.d("=== END BILLING PROGRAMS INITIALIZATION ===", TAG)
+ }
+
billingClient = builder.build()
OpenIapLog.d("=== buildBillingClient END ===", TAG)
}
@@ -1147,4 +1444,68 @@ class OpenIapModule(
override fun setUserChoiceBillingListener(listener: dev.hyo.openiap.listener.UserChoiceBillingListener?) {
userChoiceBillingListener = listener
}
+
+ /**
+ * Apply SubscriptionProductReplacementParams to ProductDetailsParams builder using reflection.
+ * This enables per-product replacement mode configuration (Billing Library 8.1.0+).
+ *
+ * @param builder The ProductDetailsParams.Builder to configure
+ * @param params The replacement parameters containing oldProductId and replacementMode
+ */
+ private fun applySubscriptionProductReplacementParams(
+ builder: BillingFlowParams.ProductDetailsParams.Builder,
+ params: SubscriptionProductReplacementParamsAndroid
+ ) {
+ try {
+ // Convert our enum to BillingClient replacement mode constant
+ val replacementModeConstant = when (params.replacementMode) {
+ SubscriptionReplacementModeAndroid.UnknownReplacementMode -> 0
+ SubscriptionReplacementModeAndroid.WithTimeProration -> 1
+ SubscriptionReplacementModeAndroid.ChargeProratedPrice -> 2
+ SubscriptionReplacementModeAndroid.WithoutProration -> 3
+ SubscriptionReplacementModeAndroid.Deferred -> 6
+ SubscriptionReplacementModeAndroid.ChargeFullPrice -> 5
+ SubscriptionReplacementModeAndroid.KeepExisting -> 7 // New in 8.1.0
+ }
+
+ // Build SubscriptionProductReplacementParams using reflection
+ val replacementParamsClass = Class.forName(
+ "com.android.billingclient.api.BillingFlowParams\$SubscriptionProductReplacementParams"
+ )
+ val replacementBuilderClass = Class.forName(
+ "com.android.billingclient.api.BillingFlowParams\$SubscriptionProductReplacementParams\$Builder"
+ )
+
+ // Create new builder
+ val newBuilderMethod = replacementParamsClass.getMethod("newBuilder")
+ val replacementBuilder = newBuilderMethod.invoke(null)
+
+ // Set old product ID
+ val setOldProductIdMethod = replacementBuilderClass.getMethod("setOldProductId", String::class.java)
+ setOldProductIdMethod.invoke(replacementBuilder, params.oldProductId)
+
+ // Set replacement mode
+ val setReplacementModeMethod = replacementBuilderClass.getMethod("setReplacementMode", Int::class.javaPrimitiveType)
+ setReplacementModeMethod.invoke(replacementBuilder, replacementModeConstant)
+
+ // Build the params
+ val buildMethod = replacementBuilderClass.getMethod("build")
+ val subscriptionReplacementParams = buildMethod.invoke(replacementBuilder)
+
+ // Apply to ProductDetailsParams builder
+ val setSubsReplacementParamsMethod = builder.javaClass.getMethod(
+ "setSubscriptionProductReplacementParams",
+ replacementParamsClass
+ )
+ setSubsReplacementParamsMethod.invoke(builder, subscriptionReplacementParams)
+
+ OpenIapLog.d("Applied SubscriptionProductReplacementParams: oldProductId=${params.oldProductId}, mode=${params.replacementMode} (constant=$replacementModeConstant)", TAG)
+ } catch (e: NoSuchMethodException) {
+ OpenIapLog.w("setSubscriptionProductReplacementParams not found. Requires Billing Library 8.1.0+. Falling back to legacy replacement mode.", TAG)
+ } catch (e: ClassNotFoundException) {
+ OpenIapLog.w("SubscriptionProductReplacementParams class not found. Requires Billing Library 8.1.0+.", TAG)
+ } catch (e: Exception) {
+ OpenIapLog.e("Failed to apply SubscriptionProductReplacementParams: ${e.message}", e, TAG)
+ }
+ }
}
diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
index 5f117ccb..ad8af070 100644
--- a/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
+++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/helpers/Helpers.kt
@@ -10,6 +10,7 @@ import dev.hyo.openiap.ProductQueryType
import dev.hyo.openiap.Purchase
import dev.hyo.openiap.PurchaseError
import dev.hyo.openiap.RequestPurchaseProps
+import dev.hyo.openiap.SubscriptionProductReplacementParamsAndroid
import dev.hyo.openiap.listener.OpenIapPurchaseErrorListener
import dev.hyo.openiap.listener.OpenIapPurchaseUpdateListener
import dev.hyo.openiap.utils.BillingConverters.toPurchase
@@ -99,6 +100,7 @@ internal data class AndroidPurchaseArgs(
val purchaseTokenAndroid: String?,
val replacementModeAndroid: Int?,
val subscriptionOffers: List?,
+ val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid?,
val type: ProductQueryType,
val useAlternativeBilling: Boolean?
)
@@ -116,6 +118,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs {
purchaseTokenAndroid = null,
replacementModeAndroid = null,
subscriptionOffers = null,
+ subscriptionProductReplacementParams = null,
type = type,
useAlternativeBilling = useAlternativeBilling
)
@@ -136,6 +139,7 @@ internal fun RequestPurchaseProps.toAndroidPurchaseArgs(): AndroidPurchaseArgs {
purchaseTokenAndroid = android.purchaseTokenAndroid,
replacementModeAndroid = android.replacementModeAndroid,
subscriptionOffers = android.subscriptionOffers,
+ subscriptionProductReplacementParams = android.subscriptionProductReplacementParams,
type = type,
useAlternativeBilling = useAlternativeBilling
)
diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
index 43b5872b..6da3ea39 100644
--- a/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
+++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/utils/BillingConverters.kt
@@ -62,16 +62,16 @@ internal object BillingConverters {
)
}
- // Extract preorder details if available
- val preorder = preorderDetails?.let { details ->
+ // Extract preorder details if available (Billing Library 8.1.0+)
+ val preorder = runCatching { preorderDetails }?.getOrNull()?.let { details ->
PreorderDetailsAndroid(
preorderPresaleEndTimeMillis = details.preorderPresaleEndTimeMillis.toString(),
preorderReleaseTimeMillis = details.preorderReleaseTimeMillis.toString()
)
}
- // Extract rental details if available
- val rental = rentalDetails?.let { details ->
+ // Extract rental details if available (Billing Library 7.0+)
+ val rental = runCatching { rentalDetails }?.getOrNull()?.let { details ->
RentalDetailsAndroid(
rentalPeriod = details.rentalPeriod,
rentalExpirationPeriod = runCatching { details.rentalExpirationPeriod }.getOrNull()
diff --git a/packages/gql/src/generated/Types.kt b/packages/gql/src/generated/Types.kt
index ce956aa6..cd1b9968 100644
--- a/packages/gql/src/generated/Types.kt
+++ b/packages/gql/src/generated/Types.kt
@@ -42,6 +42,41 @@ public enum class AlternativeBillingModeAndroid(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * Billing program types for external content links and external offers (Android)
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public enum class BillingProgramAndroid(val rawValue: String) {
+ /**
+ * Unspecified billing program. Do not use.
+ */
+ Unspecified("unspecified"),
+ /**
+ * External Content Links program.
+ * Allows linking to external content outside the app.
+ */
+ ExternalContentLink("external-content-link"),
+ /**
+ * External Offers program.
+ * Allows offering digital content purchases outside the app.
+ */
+ ExternalOffer("external-offer")
+
+ companion object {
+ fun fromJson(value: String): BillingProgramAndroid = when (value) {
+ "unspecified" -> BillingProgramAndroid.Unspecified
+ "UNSPECIFIED" -> BillingProgramAndroid.Unspecified
+ "external-content-link" -> BillingProgramAndroid.ExternalContentLink
+ "EXTERNAL_CONTENT_LINK" -> BillingProgramAndroid.ExternalContentLink
+ "external-offer" -> BillingProgramAndroid.ExternalOffer
+ "EXTERNAL_OFFER" -> BillingProgramAndroid.ExternalOffer
+ else -> throw IllegalArgumentException("Unknown BillingProgramAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
public enum class ErrorCode(val rawValue: String) {
Unknown("unknown"),
UserCancelled("user-cancelled"),
@@ -201,6 +236,74 @@ public enum class ErrorCode(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * Launch mode for external link flow (Android)
+ * Determines how the external URL is launched
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public enum class ExternalLinkLaunchModeAndroid(val rawValue: String) {
+ /**
+ * Unspecified launch mode. Do not use.
+ */
+ Unspecified("unspecified"),
+ /**
+ * Play will launch the URL in an external browser or eligible app
+ */
+ LaunchInExternalBrowserOrApp("launch-in-external-browser-or-app"),
+ /**
+ * Play will not launch the URL. The app handles launching the URL after Play returns control.
+ */
+ CallerWillLaunchLink("caller-will-launch-link")
+
+ companion object {
+ fun fromJson(value: String): ExternalLinkLaunchModeAndroid = when (value) {
+ "unspecified" -> ExternalLinkLaunchModeAndroid.Unspecified
+ "UNSPECIFIED" -> ExternalLinkLaunchModeAndroid.Unspecified
+ "launch-in-external-browser-or-app" -> ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp
+ "LAUNCH_IN_EXTERNAL_BROWSER_OR_APP" -> ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp
+ "caller-will-launch-link" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink
+ "CALLER_WILL_LAUNCH_LINK" -> ExternalLinkLaunchModeAndroid.CallerWillLaunchLink
+ else -> throw IllegalArgumentException("Unknown ExternalLinkLaunchModeAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
+/**
+ * Link type for external link flow (Android)
+ * Specifies the type of external link destination
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public enum class ExternalLinkTypeAndroid(val rawValue: String) {
+ /**
+ * Unspecified link type. Do not use.
+ */
+ Unspecified("unspecified"),
+ /**
+ * The link will direct users to a digital content offer
+ */
+ LinkToDigitalContentOffer("link-to-digital-content-offer"),
+ /**
+ * The link will direct users to download an app
+ */
+ LinkToAppDownload("link-to-app-download")
+
+ companion object {
+ fun fromJson(value: String): ExternalLinkTypeAndroid = when (value) {
+ "unspecified" -> ExternalLinkTypeAndroid.Unspecified
+ "UNSPECIFIED" -> ExternalLinkTypeAndroid.Unspecified
+ "link-to-digital-content-offer" -> ExternalLinkTypeAndroid.LinkToDigitalContentOffer
+ "LINK_TO_DIGITAL_CONTENT_OFFER" -> ExternalLinkTypeAndroid.LinkToDigitalContentOffer
+ "link-to-app-download" -> ExternalLinkTypeAndroid.LinkToAppDownload
+ "LINK_TO_APP_DOWNLOAD" -> ExternalLinkTypeAndroid.LinkToAppDownload
+ else -> throw IllegalArgumentException("Unknown ExternalLinkTypeAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
/**
* User actions on external purchase notice sheet (iOS 18.2+)
*/
@@ -565,6 +668,64 @@ public enum class SubscriptionPeriodIOS(val rawValue: String) {
fun toJson(): String = rawValue
}
+/**
+ * Replacement mode for subscription changes (Android)
+ * These modes determine how the subscription replacement affects billing.
+ * Available in Google Play Billing Library 8.1.0+
+ */
+public enum class SubscriptionReplacementModeAndroid(val rawValue: String) {
+ /**
+ * Unknown replacement mode. Do not use.
+ */
+ UnknownReplacementMode("unknown-replacement-mode"),
+ /**
+ * Replacement takes effect immediately, and the new expiration time will be prorated.
+ */
+ WithTimeProration("with-time-proration"),
+ /**
+ * Replacement takes effect immediately, and the billing cycle remains the same.
+ */
+ ChargeProratedPrice("charge-prorated-price"),
+ /**
+ * Replacement takes effect immediately, and the user is charged full price immediately.
+ */
+ ChargeFullPrice("charge-full-price"),
+ /**
+ * Replacement takes effect when the old plan expires.
+ */
+ WithoutProration("without-proration"),
+ /**
+ * Replacement takes effect when the old plan expires, and the user is not charged.
+ */
+ Deferred("deferred"),
+ /**
+ * Keep the existing payment schedule unchanged for the item (8.1.0+)
+ */
+ KeepExisting("keep-existing")
+
+ companion object {
+ fun fromJson(value: String): SubscriptionReplacementModeAndroid = when (value) {
+ "unknown-replacement-mode" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode
+ "UNKNOWN_REPLACEMENT_MODE" -> SubscriptionReplacementModeAndroid.UnknownReplacementMode
+ "with-time-proration" -> SubscriptionReplacementModeAndroid.WithTimeProration
+ "WITH_TIME_PRORATION" -> SubscriptionReplacementModeAndroid.WithTimeProration
+ "charge-prorated-price" -> SubscriptionReplacementModeAndroid.ChargeProratedPrice
+ "CHARGE_PRORATED_PRICE" -> SubscriptionReplacementModeAndroid.ChargeProratedPrice
+ "charge-full-price" -> SubscriptionReplacementModeAndroid.ChargeFullPrice
+ "CHARGE_FULL_PRICE" -> SubscriptionReplacementModeAndroid.ChargeFullPrice
+ "without-proration" -> SubscriptionReplacementModeAndroid.WithoutProration
+ "WITHOUT_PRORATION" -> SubscriptionReplacementModeAndroid.WithoutProration
+ "deferred" -> SubscriptionReplacementModeAndroid.Deferred
+ "DEFERRED" -> SubscriptionReplacementModeAndroid.Deferred
+ "keep-existing" -> SubscriptionReplacementModeAndroid.KeepExisting
+ "KEEP_EXISTING" -> SubscriptionReplacementModeAndroid.KeepExisting
+ else -> throw IllegalArgumentException("Unknown SubscriptionReplacementModeAndroid value: $value")
+ }
+ }
+
+ fun toJson(): String = rawValue
+}
+
// MARK: - Interfaces
public interface ProductCommon {
@@ -737,6 +898,70 @@ public data class AppTransaction(
)
}
+/**
+ * Result of checking billing program availability (Android)
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public data class BillingProgramAvailabilityResultAndroid(
+ /**
+ * The billing program that was checked
+ */
+ val billingProgram: BillingProgramAndroid,
+ /**
+ * Whether the billing program is available for the user
+ */
+ val isAvailable: Boolean
+) {
+
+ companion object {
+ fun fromJson(json: Map): BillingProgramAvailabilityResultAndroid {
+ return BillingProgramAvailabilityResultAndroid(
+ billingProgram = BillingProgramAndroid.fromJson(json["billingProgram"] as String),
+ isAvailable = json["isAvailable"] as Boolean,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "BillingProgramAvailabilityResultAndroid",
+ "billingProgram" to billingProgram.toJson(),
+ "isAvailable" to isAvailable,
+ )
+}
+
+/**
+ * Reporting details for transactions made outside of Google Play Billing (Android)
+ * Contains the external transaction token needed for reporting
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public data class BillingProgramReportingDetailsAndroid(
+ /**
+ * The billing program that the reporting details are associated with
+ */
+ val billingProgram: BillingProgramAndroid,
+ /**
+ * External transaction token used to report transactions made outside of Google Play Billing.
+ * This token must be used when reporting the external transaction to Google.
+ */
+ val externalTransactionToken: String
+) {
+
+ companion object {
+ fun fromJson(json: Map): BillingProgramReportingDetailsAndroid {
+ return BillingProgramReportingDetailsAndroid(
+ billingProgram = BillingProgramAndroid.fromJson(json["billingProgram"] as String),
+ externalTransactionToken = json["externalTransactionToken"] as String,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "BillingProgramReportingDetailsAndroid",
+ "billingProgram" to billingProgram.toJson(),
+ "externalTransactionToken" to externalTransactionToken,
+ )
+}
+
/**
* Discount amount details for one-time purchase offers (Android)
* Available in Google Play Billing Library 7.0+
@@ -909,6 +1134,58 @@ public data class EntitlementIOS(
)
}
+/**
+ * External offer availability result (Android)
+ * @deprecated Use BillingProgramAvailabilityResultAndroid with isBillingProgramAvailableAsync instead
+ * Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+ */
+public data class ExternalOfferAvailabilityResultAndroid(
+ /**
+ * Whether external offers are available for the user
+ */
+ val isAvailable: Boolean
+) {
+
+ companion object {
+ fun fromJson(json: Map): ExternalOfferAvailabilityResultAndroid {
+ return ExternalOfferAvailabilityResultAndroid(
+ isAvailable = json["isAvailable"] as Boolean,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "ExternalOfferAvailabilityResultAndroid",
+ "isAvailable" to isAvailable,
+ )
+}
+
+/**
+ * External offer reporting details (Android)
+ * @deprecated Use BillingProgramReportingDetailsAndroid with createBillingProgramReportingDetailsAsync instead
+ * Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+ */
+public data class ExternalOfferReportingDetailsAndroid(
+ /**
+ * External transaction token for reporting external offer transactions
+ */
+ val externalTransactionToken: String
+) {
+
+ companion object {
+ fun fromJson(json: Map): ExternalOfferReportingDetailsAndroid {
+ return ExternalOfferReportingDetailsAndroid(
+ externalTransactionToken = json["externalTransactionToken"] as String,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "__typename" to "ExternalOfferReportingDetailsAndroid",
+ "externalTransactionToken" to externalTransactionToken,
+ )
+}
+
/**
* Result of presenting an external purchase link (iOS 18.2+)
*/
@@ -2341,6 +2618,48 @@ public data class InitConnectionConfig(
)
}
+/**
+ * Parameters for launching an external link (Android)
+ * Used with launchExternalLink to initiate external offer or app install flows
+ * Available in Google Play Billing Library 8.2.0+
+ */
+public data class LaunchExternalLinkParamsAndroid(
+ /**
+ * The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ */
+ val billingProgram: BillingProgramAndroid,
+ /**
+ * The external link launch mode
+ */
+ val launchMode: ExternalLinkLaunchModeAndroid,
+ /**
+ * The type of the external link
+ */
+ val linkType: ExternalLinkTypeAndroid,
+ /**
+ * The URI where the content will be accessed from
+ */
+ val linkUri: String
+) {
+ companion object {
+ fun fromJson(json: Map): LaunchExternalLinkParamsAndroid {
+ return LaunchExternalLinkParamsAndroid(
+ billingProgram = BillingProgramAndroid.fromJson(json["billingProgram"] as String),
+ launchMode = ExternalLinkLaunchModeAndroid.fromJson(json["launchMode"] as String),
+ linkType = ExternalLinkTypeAndroid.fromJson(json["linkType"] as String),
+ linkUri = json["linkUri"] as String,
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "billingProgram" to billingProgram.toJson(),
+ "launchMode" to launchMode.toJson(),
+ "linkType" to linkType.toJson(),
+ "linkUri" to linkUri,
+ )
+}
+
public data class ProductRequest(
val skus: List,
val type: ProductQueryType? = null
@@ -2576,6 +2895,7 @@ public data class RequestSubscriptionAndroidProps(
val purchaseTokenAndroid: String? = null,
/**
* Replacement mode for subscription changes
+ * @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+)
*/
val replacementModeAndroid: Int? = null,
/**
@@ -2585,7 +2905,12 @@ public data class RequestSubscriptionAndroidProps(
/**
* Subscription offers
*/
- val subscriptionOffers: List? = null
+ val subscriptionOffers: List? = null,
+ /**
+ * Product-level replacement parameters (8.1.0+)
+ * Use this instead of replacementModeAndroid for item-level replacement
+ */
+ val subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = null
) {
companion object {
fun fromJson(json: Map): RequestSubscriptionAndroidProps {
@@ -2597,6 +2922,7 @@ public data class RequestSubscriptionAndroidProps(
replacementModeAndroid = (json["replacementModeAndroid"] as Number?)?.toInt(),
skus = (json["skus"] as List<*>).map { it as String },
subscriptionOffers = (json["subscriptionOffers"] as List<*>?)?.map { AndroidSubscriptionOfferInput.fromJson((it as Map)) },
+ subscriptionProductReplacementParams = (json["subscriptionProductReplacementParams"] as Map?)?.let { SubscriptionProductReplacementParamsAndroid.fromJson(it) },
)
}
}
@@ -2609,6 +2935,7 @@ public data class RequestSubscriptionAndroidProps(
"replacementModeAndroid" to replacementModeAndroid,
"skus" to skus.map { it },
"subscriptionOffers" to subscriptionOffers?.map { it.toJson() },
+ "subscriptionProductReplacementParams" to subscriptionProductReplacementParams?.toJson(),
)
}
@@ -2746,6 +3073,36 @@ public data class RequestVerifyPurchaseWithIapkitProps(
)
}
+/**
+ * Product-level subscription replacement parameters (Android)
+ * Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
+ * Available in Google Play Billing Library 8.1.0+
+ */
+public data class SubscriptionProductReplacementParamsAndroid(
+ /**
+ * The old product ID that needs to be replaced
+ */
+ val oldProductId: String,
+ /**
+ * The replacement mode for this product change
+ */
+ val replacementMode: SubscriptionReplacementModeAndroid
+) {
+ companion object {
+ fun fromJson(json: Map): SubscriptionProductReplacementParamsAndroid {
+ return SubscriptionProductReplacementParamsAndroid(
+ oldProductId = json["oldProductId"] as String,
+ replacementMode = SubscriptionReplacementModeAndroid.fromJson(json["replacementMode"] as String),
+ )
+ }
+ }
+
+ fun toJson(): Map = mapOf(
+ "oldProductId" to oldProductId,
+ "replacementMode" to replacementMode.toJson(),
+ )
+}
+
public data class VerifyPurchaseAndroidOptions(
val accessToken: String,
val isSub: Boolean? = null,
diff --git a/packages/gql/src/generated/Types.swift b/packages/gql/src/generated/Types.swift
index 1d17f520..e4681362 100644
--- a/packages/gql/src/generated/Types.swift
+++ b/packages/gql/src/generated/Types.swift
@@ -20,6 +20,19 @@ public enum AlternativeBillingModeAndroid: String, Codable, CaseIterable {
case alternativeOnly = "alternative-only"
}
+/// Billing program types for external content links and external offers (Android)
+/// Available in Google Play Billing Library 8.2.0+
+public enum BillingProgramAndroid: String, Codable, CaseIterable {
+ /// Unspecified billing program. Do not use.
+ case unspecified = "unspecified"
+ /// External Content Links program.
+ /// Allows linking to external content outside the app.
+ case externalContentLink = "external-content-link"
+ /// External Offers program.
+ /// Allows offering digital content purchases outside the app.
+ case externalOffer = "external-offer"
+}
+
public enum ErrorCode: String, Codable, CaseIterable {
case unknown = "unknown"
case userCancelled = "user-cancelled"
@@ -144,6 +157,30 @@ public enum ErrorCode: String, Codable, CaseIterable {
}
}
+/// Launch mode for external link flow (Android)
+/// Determines how the external URL is launched
+/// Available in Google Play Billing Library 8.2.0+
+public enum ExternalLinkLaunchModeAndroid: String, Codable, CaseIterable {
+ /// Unspecified launch mode. Do not use.
+ case unspecified = "unspecified"
+ /// Play will launch the URL in an external browser or eligible app
+ case launchInExternalBrowserOrApp = "launch-in-external-browser-or-app"
+ /// Play will not launch the URL. The app handles launching the URL after Play returns control.
+ case callerWillLaunchLink = "caller-will-launch-link"
+}
+
+/// Link type for external link flow (Android)
+/// Specifies the type of external link destination
+/// Available in Google Play Billing Library 8.2.0+
+public enum ExternalLinkTypeAndroid: String, Codable, CaseIterable {
+ /// Unspecified link type. Do not use.
+ case unspecified = "unspecified"
+ /// The link will direct users to a digital content offer
+ case linkToDigitalContentOffer = "link-to-digital-content-offer"
+ /// The link will direct users to download an app
+ case linkToAppDownload = "link-to-app-download"
+}
+
/// User actions on external purchase notice sheet (iOS 18.2+)
public enum ExternalPurchaseNoticeAction: String, Codable, CaseIterable {
/// User chose to continue to external purchase
@@ -244,6 +281,26 @@ public enum SubscriptionPeriodIOS: String, Codable, CaseIterable {
case empty = "empty"
}
+/// Replacement mode for subscription changes (Android)
+/// These modes determine how the subscription replacement affects billing.
+/// Available in Google Play Billing Library 8.1.0+
+public enum SubscriptionReplacementModeAndroid: String, Codable, CaseIterable {
+ /// Unknown replacement mode. Do not use.
+ case unknownReplacementMode = "unknown-replacement-mode"
+ /// Replacement takes effect immediately, and the new expiration time will be prorated.
+ case withTimeProration = "with-time-proration"
+ /// Replacement takes effect immediately, and the billing cycle remains the same.
+ case chargeProratedPrice = "charge-prorated-price"
+ /// Replacement takes effect immediately, and the user is charged full price immediately.
+ case chargeFullPrice = "charge-full-price"
+ /// Replacement takes effect when the old plan expires.
+ case withoutProration = "without-proration"
+ /// Replacement takes effect when the old plan expires, and the user is not charged.
+ case deferred = "deferred"
+ /// Keep the existing payment schedule unchanged for the item (8.1.0+)
+ case keepExisting = "keep-existing"
+}
+
// MARK: - Interfaces
public protocol ProductCommon: Codable {
@@ -324,6 +381,26 @@ public struct AppTransaction: Codable {
public var signedDate: Double
}
+/// Result of checking billing program availability (Android)
+/// Available in Google Play Billing Library 8.2.0+
+public struct BillingProgramAvailabilityResultAndroid: Codable {
+ /// The billing program that was checked
+ public var billingProgram: BillingProgramAndroid
+ /// Whether the billing program is available for the user
+ public var isAvailable: Bool
+}
+
+/// Reporting details for transactions made outside of Google Play Billing (Android)
+/// Contains the external transaction token needed for reporting
+/// Available in Google Play Billing Library 8.2.0+
+public struct BillingProgramReportingDetailsAndroid: Codable {
+ /// The billing program that the reporting details are associated with
+ public var billingProgram: BillingProgramAndroid
+ /// External transaction token used to report transactions made outside of Google Play Billing.
+ /// This token must be used when reporting the external transaction to Google.
+ public var externalTransactionToken: String
+}
+
/// Discount amount details for one-time purchase offers (Android)
/// Available in Google Play Billing Library 7.0+
public struct DiscountAmountAndroid: Codable {
@@ -374,6 +451,22 @@ public struct EntitlementIOS: Codable {
public var transactionId: String
}
+/// External offer availability result (Android)
+/// @deprecated Use BillingProgramAvailabilityResultAndroid with isBillingProgramAvailableAsync instead
+/// Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+public struct ExternalOfferAvailabilityResultAndroid: Codable {
+ /// Whether external offers are available for the user
+ public var isAvailable: Bool
+}
+
+/// External offer reporting details (Android)
+/// @deprecated Use BillingProgramReportingDetailsAndroid with createBillingProgramReportingDetailsAsync instead
+/// Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+public struct ExternalOfferReportingDetailsAndroid: Codable {
+ /// External transaction token for reporting external offer transactions
+ public var externalTransactionToken: String
+}
+
/// Result of presenting an external purchase link (iOS 18.2+)
public struct ExternalPurchaseLinkResultIOS: Codable {
/// Optional error message if the presentation failed
@@ -873,6 +966,32 @@ public struct InitConnectionConfig: Codable {
}
}
+/// Parameters for launching an external link (Android)
+/// Used with launchExternalLink to initiate external offer or app install flows
+/// Available in Google Play Billing Library 8.2.0+
+public struct LaunchExternalLinkParamsAndroid: Codable {
+ /// The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ public var billingProgram: BillingProgramAndroid
+ /// The external link launch mode
+ public var launchMode: ExternalLinkLaunchModeAndroid
+ /// The type of the external link
+ public var linkType: ExternalLinkTypeAndroid
+ /// The URI where the content will be accessed from
+ public var linkUri: String
+
+ public init(
+ billingProgram: BillingProgramAndroid,
+ launchMode: ExternalLinkLaunchModeAndroid,
+ linkType: ExternalLinkTypeAndroid,
+ linkUri: String
+ ) {
+ self.billingProgram = billingProgram
+ self.launchMode = launchMode
+ self.linkType = linkType
+ self.linkUri = linkUri
+ }
+}
+
public struct ProductRequest: Codable {
public var skus: [String]
public var type: ProductQueryType?
@@ -1056,11 +1175,15 @@ public struct RequestSubscriptionAndroidProps: Codable {
/// Purchase token for upgrades/downgrades
public var purchaseTokenAndroid: String?
/// Replacement mode for subscription changes
+ /// @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+)
public var replacementModeAndroid: Int?
/// List of subscription SKUs
public var skus: [String]
/// Subscription offers
public var subscriptionOffers: [AndroidSubscriptionOfferInput]?
+ /// Product-level replacement parameters (8.1.0+)
+ /// Use this instead of replacementModeAndroid for item-level replacement
+ public var subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid?
public init(
isOfferPersonalized: Bool? = nil,
@@ -1069,7 +1192,8 @@ public struct RequestSubscriptionAndroidProps: Codable {
purchaseTokenAndroid: String? = nil,
replacementModeAndroid: Int? = nil,
skus: [String],
- subscriptionOffers: [AndroidSubscriptionOfferInput]? = nil
+ subscriptionOffers: [AndroidSubscriptionOfferInput]? = nil,
+ subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid? = nil
) {
self.isOfferPersonalized = isOfferPersonalized
self.obfuscatedAccountIdAndroid = obfuscatedAccountIdAndroid
@@ -1078,6 +1202,7 @@ public struct RequestSubscriptionAndroidProps: Codable {
self.replacementModeAndroid = replacementModeAndroid
self.skus = skus
self.subscriptionOffers = subscriptionOffers
+ self.subscriptionProductReplacementParams = subscriptionProductReplacementParams
}
}
@@ -1167,6 +1292,24 @@ public struct RequestVerifyPurchaseWithIapkitProps: Codable {
}
}
+/// Product-level subscription replacement parameters (Android)
+/// Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
+/// Available in Google Play Billing Library 8.1.0+
+public struct SubscriptionProductReplacementParamsAndroid: Codable {
+ /// The old product ID that needs to be replaced
+ public var oldProductId: String
+ /// The replacement mode for this product change
+ public var replacementMode: SubscriptionReplacementModeAndroid
+
+ public init(
+ oldProductId: String,
+ replacementMode: SubscriptionReplacementModeAndroid
+ ) {
+ self.oldProductId = oldProductId
+ self.replacementMode = replacementMode
+ }
+}
+
public struct VerifyPurchaseAndroidOptions: Codable {
public var accessToken: String
public var isSub: Bool?
diff --git a/packages/gql/src/generated/types.dart b/packages/gql/src/generated/types.dart
index e8417cdd..b79e0451 100644
--- a/packages/gql/src/generated/types.dart
+++ b/packages/gql/src/generated/types.dart
@@ -42,6 +42,39 @@ enum AlternativeBillingModeAndroid {
String toJson() => value;
}
+/// Billing program types for external content links and external offers (Android)
+/// Available in Google Play Billing Library 8.2.0+
+enum BillingProgramAndroid {
+ /// Unspecified billing program. Do not use.
+ Unspecified('unspecified'),
+ /// External Content Links program.
+ /// Allows linking to external content outside the app.
+ ExternalContentLink('external-content-link'),
+ /// External Offers program.
+ /// Allows offering digital content purchases outside the app.
+ ExternalOffer('external-offer');
+
+ const BillingProgramAndroid(this.value);
+ final String value;
+
+ factory BillingProgramAndroid.fromJson(String value) {
+ switch (value) {
+ case 'unspecified':
+ case 'UNSPECIFIED':
+ return BillingProgramAndroid.Unspecified;
+ case 'external-content-link':
+ case 'EXTERNAL_CONTENT_LINK':
+ return BillingProgramAndroid.ExternalContentLink;
+ case 'external-offer':
+ case 'EXTERNAL_OFFER':
+ return BillingProgramAndroid.ExternalOffer;
+ }
+ throw ArgumentError('Unknown BillingProgramAndroid value: $value');
+ }
+
+ String toJson() => value;
+}
+
enum ErrorCode {
Unknown('unknown'),
UserCancelled('user-cancelled'),
@@ -241,6 +274,70 @@ enum ErrorCode {
String toJson() => value;
}
+/// Launch mode for external link flow (Android)
+/// Determines how the external URL is launched
+/// Available in Google Play Billing Library 8.2.0+
+enum ExternalLinkLaunchModeAndroid {
+ /// Unspecified launch mode. Do not use.
+ Unspecified('unspecified'),
+ /// Play will launch the URL in an external browser or eligible app
+ LaunchInExternalBrowserOrApp('launch-in-external-browser-or-app'),
+ /// Play will not launch the URL. The app handles launching the URL after Play returns control.
+ CallerWillLaunchLink('caller-will-launch-link');
+
+ const ExternalLinkLaunchModeAndroid(this.value);
+ final String value;
+
+ factory ExternalLinkLaunchModeAndroid.fromJson(String value) {
+ switch (value) {
+ case 'unspecified':
+ case 'UNSPECIFIED':
+ return ExternalLinkLaunchModeAndroid.Unspecified;
+ case 'launch-in-external-browser-or-app':
+ case 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP':
+ return ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp;
+ case 'caller-will-launch-link':
+ case 'CALLER_WILL_LAUNCH_LINK':
+ return ExternalLinkLaunchModeAndroid.CallerWillLaunchLink;
+ }
+ throw ArgumentError('Unknown ExternalLinkLaunchModeAndroid value: $value');
+ }
+
+ String toJson() => value;
+}
+
+/// Link type for external link flow (Android)
+/// Specifies the type of external link destination
+/// Available in Google Play Billing Library 8.2.0+
+enum ExternalLinkTypeAndroid {
+ /// Unspecified link type. Do not use.
+ Unspecified('unspecified'),
+ /// The link will direct users to a digital content offer
+ LinkToDigitalContentOffer('link-to-digital-content-offer'),
+ /// The link will direct users to download an app
+ LinkToAppDownload('link-to-app-download');
+
+ const ExternalLinkTypeAndroid(this.value);
+ final String value;
+
+ factory ExternalLinkTypeAndroid.fromJson(String value) {
+ switch (value) {
+ case 'unspecified':
+ case 'UNSPECIFIED':
+ return ExternalLinkTypeAndroid.Unspecified;
+ case 'link-to-digital-content-offer':
+ case 'LINK_TO_DIGITAL_CONTENT_OFFER':
+ return ExternalLinkTypeAndroid.LinkToDigitalContentOffer;
+ case 'link-to-app-download':
+ case 'LINK_TO_APP_DOWNLOAD':
+ return ExternalLinkTypeAndroid.LinkToAppDownload;
+ }
+ throw ArgumentError('Unknown ExternalLinkTypeAndroid value: $value');
+ }
+
+ String toJson() => value;
+}
+
/// User actions on external purchase notice sheet (iOS 18.2+)
enum ExternalPurchaseNoticeAction {
/// User chose to continue to external purchase
@@ -666,6 +763,58 @@ enum SubscriptionPeriodIOS {
String toJson() => value;
}
+/// Replacement mode for subscription changes (Android)
+/// These modes determine how the subscription replacement affects billing.
+/// Available in Google Play Billing Library 8.1.0+
+enum SubscriptionReplacementModeAndroid {
+ /// Unknown replacement mode. Do not use.
+ UnknownReplacementMode('unknown-replacement-mode'),
+ /// Replacement takes effect immediately, and the new expiration time will be prorated.
+ WithTimeProration('with-time-proration'),
+ /// Replacement takes effect immediately, and the billing cycle remains the same.
+ ChargeProratedPrice('charge-prorated-price'),
+ /// Replacement takes effect immediately, and the user is charged full price immediately.
+ ChargeFullPrice('charge-full-price'),
+ /// Replacement takes effect when the old plan expires.
+ WithoutProration('without-proration'),
+ /// Replacement takes effect when the old plan expires, and the user is not charged.
+ Deferred('deferred'),
+ /// Keep the existing payment schedule unchanged for the item (8.1.0+)
+ KeepExisting('keep-existing');
+
+ const SubscriptionReplacementModeAndroid(this.value);
+ final String value;
+
+ factory SubscriptionReplacementModeAndroid.fromJson(String value) {
+ switch (value) {
+ case 'unknown-replacement-mode':
+ case 'UNKNOWN_REPLACEMENT_MODE':
+ return SubscriptionReplacementModeAndroid.UnknownReplacementMode;
+ case 'with-time-proration':
+ case 'WITH_TIME_PRORATION':
+ return SubscriptionReplacementModeAndroid.WithTimeProration;
+ case 'charge-prorated-price':
+ case 'CHARGE_PRORATED_PRICE':
+ return SubscriptionReplacementModeAndroid.ChargeProratedPrice;
+ case 'charge-full-price':
+ case 'CHARGE_FULL_PRICE':
+ return SubscriptionReplacementModeAndroid.ChargeFullPrice;
+ case 'without-proration':
+ case 'WITHOUT_PRORATION':
+ return SubscriptionReplacementModeAndroid.WithoutProration;
+ case 'deferred':
+ case 'DEFERRED':
+ return SubscriptionReplacementModeAndroid.Deferred;
+ case 'keep-existing':
+ case 'KEEP_EXISTING':
+ return SubscriptionReplacementModeAndroid.KeepExisting;
+ }
+ throw ArgumentError('Unknown SubscriptionReplacementModeAndroid value: $value');
+ }
+
+ String toJson() => value;
+}
+
// MARK: - Interfaces
abstract class ProductCommon {
@@ -865,6 +1014,71 @@ class AppTransaction {
}
}
+/// Result of checking billing program availability (Android)
+/// Available in Google Play Billing Library 8.2.0+
+class BillingProgramAvailabilityResultAndroid {
+ const BillingProgramAvailabilityResultAndroid({
+ /// The billing program that was checked
+ required this.billingProgram,
+ /// Whether the billing program is available for the user
+ required this.isAvailable,
+ });
+
+ /// The billing program that was checked
+ final BillingProgramAndroid billingProgram;
+ /// Whether the billing program is available for the user
+ final bool isAvailable;
+
+ factory BillingProgramAvailabilityResultAndroid.fromJson(Map json) {
+ return BillingProgramAvailabilityResultAndroid(
+ billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String),
+ isAvailable: json['isAvailable'] as bool,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'BillingProgramAvailabilityResultAndroid',
+ 'billingProgram': billingProgram.toJson(),
+ 'isAvailable': isAvailable,
+ };
+ }
+}
+
+/// Reporting details for transactions made outside of Google Play Billing (Android)
+/// Contains the external transaction token needed for reporting
+/// Available in Google Play Billing Library 8.2.0+
+class BillingProgramReportingDetailsAndroid {
+ const BillingProgramReportingDetailsAndroid({
+ /// The billing program that the reporting details are associated with
+ required this.billingProgram,
+ /// External transaction token used to report transactions made outside of Google Play Billing.
+ /// This token must be used when reporting the external transaction to Google.
+ required this.externalTransactionToken,
+ });
+
+ /// The billing program that the reporting details are associated with
+ final BillingProgramAndroid billingProgram;
+ /// External transaction token used to report transactions made outside of Google Play Billing.
+ /// This token must be used when reporting the external transaction to Google.
+ final String externalTransactionToken;
+
+ factory BillingProgramReportingDetailsAndroid.fromJson(Map json) {
+ return BillingProgramReportingDetailsAndroid(
+ billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String),
+ externalTransactionToken: json['externalTransactionToken'] as String,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'BillingProgramReportingDetailsAndroid',
+ 'billingProgram': billingProgram.toJson(),
+ 'externalTransactionToken': externalTransactionToken,
+ };
+ }
+}
+
/// Discount amount details for one-time purchase offers (Android)
/// Available in Google Play Billing Library 7.0+
class DiscountAmountAndroid {
@@ -1056,6 +1270,58 @@ class EntitlementIOS {
}
}
+/// External offer availability result (Android)
+/// @deprecated Use BillingProgramAvailabilityResultAndroid with isBillingProgramAvailableAsync instead
+/// Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+class ExternalOfferAvailabilityResultAndroid {
+ const ExternalOfferAvailabilityResultAndroid({
+ /// Whether external offers are available for the user
+ required this.isAvailable,
+ });
+
+ /// Whether external offers are available for the user
+ final bool isAvailable;
+
+ factory ExternalOfferAvailabilityResultAndroid.fromJson(Map json) {
+ return ExternalOfferAvailabilityResultAndroid(
+ isAvailable: json['isAvailable'] as bool,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'ExternalOfferAvailabilityResultAndroid',
+ 'isAvailable': isAvailable,
+ };
+ }
+}
+
+/// External offer reporting details (Android)
+/// @deprecated Use BillingProgramReportingDetailsAndroid with createBillingProgramReportingDetailsAsync instead
+/// Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+class ExternalOfferReportingDetailsAndroid {
+ const ExternalOfferReportingDetailsAndroid({
+ /// External transaction token for reporting external offer transactions
+ required this.externalTransactionToken,
+ });
+
+ /// External transaction token for reporting external offer transactions
+ final String externalTransactionToken;
+
+ factory ExternalOfferReportingDetailsAndroid.fromJson(Map json) {
+ return ExternalOfferReportingDetailsAndroid(
+ externalTransactionToken: json['externalTransactionToken'] as String,
+ );
+ }
+
+ Map toJson() {
+ return {
+ '__typename': 'ExternalOfferReportingDetailsAndroid',
+ 'externalTransactionToken': externalTransactionToken,
+ };
+ }
+}
+
/// Result of presenting an external purchase link (iOS 18.2+)
class ExternalPurchaseLinkResultIOS {
const ExternalPurchaseLinkResultIOS({
@@ -2772,6 +3038,49 @@ class InitConnectionConfig {
}
}
+/// Parameters for launching an external link (Android)
+/// Used with launchExternalLink to initiate external offer or app install flows
+/// Available in Google Play Billing Library 8.2.0+
+class LaunchExternalLinkParamsAndroid {
+ const LaunchExternalLinkParamsAndroid({
+ /// The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ required this.billingProgram,
+ /// The external link launch mode
+ required this.launchMode,
+ /// The type of the external link
+ required this.linkType,
+ /// The URI where the content will be accessed from
+ required this.linkUri,
+ });
+
+ /// The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ final BillingProgramAndroid billingProgram;
+ /// The external link launch mode
+ final ExternalLinkLaunchModeAndroid launchMode;
+ /// The type of the external link
+ final ExternalLinkTypeAndroid linkType;
+ /// The URI where the content will be accessed from
+ final String linkUri;
+
+ factory LaunchExternalLinkParamsAndroid.fromJson(Map json) {
+ return LaunchExternalLinkParamsAndroid(
+ billingProgram: BillingProgramAndroid.fromJson(json['billingProgram'] as String),
+ launchMode: ExternalLinkLaunchModeAndroid.fromJson(json['launchMode'] as String),
+ linkType: ExternalLinkTypeAndroid.fromJson(json['linkType'] as String),
+ linkUri: json['linkUri'] as String,
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'billingProgram': billingProgram.toJson(),
+ 'launchMode': launchMode.toJson(),
+ 'linkType': linkType.toJson(),
+ 'linkUri': linkUri,
+ };
+ }
+}
+
class ProductRequest {
const ProductRequest({
required this.skus,
@@ -3023,11 +3332,15 @@ class RequestSubscriptionAndroidProps {
/// Purchase token for upgrades/downgrades
this.purchaseTokenAndroid,
/// Replacement mode for subscription changes
+ /// @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+)
this.replacementModeAndroid,
/// List of subscription SKUs
required this.skus,
/// Subscription offers
this.subscriptionOffers,
+ /// Product-level replacement parameters (8.1.0+)
+ /// Use this instead of replacementModeAndroid for item-level replacement
+ this.subscriptionProductReplacementParams,
});
/// Personalized offer flag
@@ -3039,11 +3352,15 @@ class RequestSubscriptionAndroidProps {
/// Purchase token for upgrades/downgrades
final String? purchaseTokenAndroid;
/// Replacement mode for subscription changes
+ /// @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+)
final int? replacementModeAndroid;
/// List of subscription SKUs
final List skus;
/// Subscription offers
final List? subscriptionOffers;
+ /// Product-level replacement parameters (8.1.0+)
+ /// Use this instead of replacementModeAndroid for item-level replacement
+ final SubscriptionProductReplacementParamsAndroid? subscriptionProductReplacementParams;
factory RequestSubscriptionAndroidProps.fromJson(Map json) {
return RequestSubscriptionAndroidProps(
@@ -3054,6 +3371,7 @@ class RequestSubscriptionAndroidProps {
replacementModeAndroid: json['replacementModeAndroid'] as int?,
skus: (json['skus'] as List).map((e) => e as String).toList(),
subscriptionOffers: (json['subscriptionOffers'] as List?) == null ? null : (json['subscriptionOffers'] as List?)!.map((e) => AndroidSubscriptionOfferInput.fromJson(e as Map)).toList(),
+ subscriptionProductReplacementParams: json['subscriptionProductReplacementParams'] != null ? SubscriptionProductReplacementParamsAndroid.fromJson(json['subscriptionProductReplacementParams'] as Map) : null,
);
}
@@ -3066,6 +3384,7 @@ class RequestSubscriptionAndroidProps {
'replacementModeAndroid': replacementModeAndroid,
'skus': skus.map((e) => e).toList(),
'subscriptionOffers': subscriptionOffers == null ? null : subscriptionOffers!.map((e) => e.toJson()).toList(),
+ 'subscriptionProductReplacementParams': subscriptionProductReplacementParams?.toJson(),
};
}
}
@@ -3224,6 +3543,37 @@ class RequestVerifyPurchaseWithIapkitProps {
}
}
+/// Product-level subscription replacement parameters (Android)
+/// Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
+/// Available in Google Play Billing Library 8.1.0+
+class SubscriptionProductReplacementParamsAndroid {
+ const SubscriptionProductReplacementParamsAndroid({
+ /// The old product ID that needs to be replaced
+ required this.oldProductId,
+ /// The replacement mode for this product change
+ required this.replacementMode,
+ });
+
+ /// The old product ID that needs to be replaced
+ final String oldProductId;
+ /// The replacement mode for this product change
+ final SubscriptionReplacementModeAndroid replacementMode;
+
+ factory SubscriptionProductReplacementParamsAndroid.fromJson(Map json) {
+ return SubscriptionProductReplacementParamsAndroid(
+ oldProductId: json['oldProductId'] as String,
+ replacementMode: SubscriptionReplacementModeAndroid.fromJson(json['replacementMode'] as String),
+ );
+ }
+
+ Map toJson() {
+ return {
+ 'oldProductId': oldProductId,
+ 'replacementMode': replacementMode.toJson(),
+ };
+ }
+}
+
class VerifyPurchaseAndroidOptions {
const VerifyPurchaseAndroidOptions({
required this.accessToken,
diff --git a/packages/gql/src/generated/types.ts b/packages/gql/src/generated/types.ts
index ab5709ef..bdd4ac65 100644
--- a/packages/gql/src/generated/types.ts
+++ b/packages/gql/src/generated/types.ts
@@ -65,6 +65,38 @@ export interface AppTransaction {
signedDate: number;
}
+/**
+ * Billing program types for external content links and external offers (Android)
+ * Available in Google Play Billing Library 8.2.0+
+ */
+export type BillingProgramAndroid = 'unspecified' | 'external-content-link' | 'external-offer';
+
+/**
+ * Result of checking billing program availability (Android)
+ * Available in Google Play Billing Library 8.2.0+
+ */
+export interface BillingProgramAvailabilityResultAndroid {
+ /** The billing program that was checked */
+ billingProgram: BillingProgramAndroid;
+ /** Whether the billing program is available for the user */
+ isAvailable: boolean;
+}
+
+/**
+ * Reporting details for transactions made outside of Google Play Billing (Android)
+ * Contains the external transaction token needed for reporting
+ * Available in Google Play Billing Library 8.2.0+
+ */
+export interface BillingProgramReportingDetailsAndroid {
+ /** The billing program that the reporting details are associated with */
+ billingProgram: BillingProgramAndroid;
+ /**
+ * External transaction token used to report transactions made outside of Google Play Billing.
+ * This token must be used when reporting the external transaction to Google.
+ */
+ externalTransactionToken: string;
+}
+
export interface DeepLinkOptions {
/** Android package name to target (required on Android) */
packageNameAndroid?: (string | null);
@@ -183,6 +215,40 @@ export enum ErrorCode {
UserError = 'user-error'
}
+/**
+ * Launch mode for external link flow (Android)
+ * Determines how the external URL is launched
+ * Available in Google Play Billing Library 8.2.0+
+ */
+export type ExternalLinkLaunchModeAndroid = 'unspecified' | 'launch-in-external-browser-or-app' | 'caller-will-launch-link';
+
+/**
+ * Link type for external link flow (Android)
+ * Specifies the type of external link destination
+ * Available in Google Play Billing Library 8.2.0+
+ */
+export type ExternalLinkTypeAndroid = 'unspecified' | 'link-to-digital-content-offer' | 'link-to-app-download';
+
+/**
+ * External offer availability result (Android)
+ * @deprecated Use BillingProgramAvailabilityResultAndroid with isBillingProgramAvailableAsync instead
+ * Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+ */
+export interface ExternalOfferAvailabilityResultAndroid {
+ /** Whether external offers are available for the user */
+ isAvailable: boolean;
+}
+
+/**
+ * External offer reporting details (Android)
+ * @deprecated Use BillingProgramReportingDetailsAndroid with createBillingProgramReportingDetailsAsync instead
+ * Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+ */
+export interface ExternalOfferReportingDetailsAndroid {
+ /** External transaction token for reporting external offer transactions */
+ externalTransactionToken: string;
+}
+
/** Result of presenting an external purchase link (iOS 18.2+) */
export interface ExternalPurchaseLinkResultIOS {
/** Optional error message if the presentation failed */
@@ -222,6 +288,22 @@ export interface InitConnectionConfig {
alternativeBillingModeAndroid?: (AlternativeBillingModeAndroid | null);
}
+/**
+ * Parameters for launching an external link (Android)
+ * Used with launchExternalLink to initiate external offer or app install flows
+ * Available in Google Play Billing Library 8.2.0+
+ */
+export interface LaunchExternalLinkParamsAndroid {
+ /** The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER) */
+ billingProgram: BillingProgramAndroid;
+ /** The external link launch mode */
+ launchMode: ExternalLinkLaunchModeAndroid;
+ /** The type of the external link */
+ linkType: ExternalLinkTypeAndroid;
+ /** The URI where the content will be accessed from */
+ linkUri: string;
+}
+
/**
* Limited quantity information for one-time purchase offers (Android)
* Available in Google Play Billing Library 7.0+
@@ -857,12 +939,20 @@ export interface RequestSubscriptionAndroidProps {
obfuscatedProfileIdAndroid?: (string | null);
/** Purchase token for upgrades/downgrades */
purchaseTokenAndroid?: (string | null);
- /** Replacement mode for subscription changes */
+ /**
+ * Replacement mode for subscription changes
+ * @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+)
+ */
replacementModeAndroid?: (number | null);
/** List of subscription SKUs */
skus: string[];
/** Subscription offers */
subscriptionOffers?: (AndroidSubscriptionOfferInput[] | null);
+ /**
+ * Product-level replacement parameters (8.1.0+)
+ * Use this instead of replacementModeAndroid for item-level replacement
+ */
+ subscriptionProductReplacementParams?: (SubscriptionProductReplacementParamsAndroid | null);
}
export interface RequestSubscriptionIosProps {
@@ -952,6 +1042,25 @@ export interface SubscriptionPeriodValueIOS {
value: number;
}
+/**
+ * Product-level subscription replacement parameters (Android)
+ * Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
+ * Available in Google Play Billing Library 8.1.0+
+ */
+export interface SubscriptionProductReplacementParamsAndroid {
+ /** The old product ID that needs to be replaced */
+ oldProductId: string;
+ /** The replacement mode for this product change */
+ replacementMode: SubscriptionReplacementModeAndroid;
+}
+
+/**
+ * Replacement mode for subscription changes (Android)
+ * These modes determine how the subscription replacement affects billing.
+ * Available in Google Play Billing Library 8.1.0+
+ */
+export type SubscriptionReplacementModeAndroid = 'unknown-replacement-mode' | 'with-time-proration' | 'charge-prorated-price' | 'charge-full-price' | 'without-proration' | 'deferred' | 'keep-existing';
+
export interface SubscriptionStatusIOS {
renewalInfo?: (RenewalInfoIOS | null);
state: string;
diff --git a/packages/gql/src/type-android.graphql b/packages/gql/src/type-android.graphql
index 83bd1330..7503c0a3 100644
--- a/packages/gql/src/type-android.graphql
+++ b/packages/gql/src/type-android.graphql
@@ -292,12 +292,71 @@ input RequestSubscriptionAndroidProps {
purchaseTokenAndroid: String
"""
Replacement mode for subscription changes
+ @deprecated Use subscriptionProductReplacementParams instead for item-level replacement (8.1.0+)
"""
replacementModeAndroid: Int
"""
Subscription offers
"""
subscriptionOffers: [AndroidSubscriptionOfferInput!]
+ """
+ Product-level replacement parameters (8.1.0+)
+ Use this instead of replacementModeAndroid for item-level replacement
+ """
+ subscriptionProductReplacementParams: SubscriptionProductReplacementParamsAndroid
+}
+
+# Subscription Replacement (Google Play Billing Library 8.1.0+)
+"""
+Replacement mode for subscription changes (Android)
+These modes determine how the subscription replacement affects billing.
+Available in Google Play Billing Library 8.1.0+
+"""
+enum SubscriptionReplacementModeAndroid {
+ """
+ Unknown replacement mode. Do not use.
+ """
+ UNKNOWN_REPLACEMENT_MODE
+ """
+ Replacement takes effect immediately, and the new expiration time will be prorated.
+ """
+ WITH_TIME_PRORATION
+ """
+ Replacement takes effect immediately, and the billing cycle remains the same.
+ """
+ CHARGE_PRORATED_PRICE
+ """
+ Replacement takes effect immediately, and the user is charged full price immediately.
+ """
+ CHARGE_FULL_PRICE
+ """
+ Replacement takes effect when the old plan expires.
+ """
+ WITHOUT_PRORATION
+ """
+ Replacement takes effect when the old plan expires, and the user is not charged.
+ """
+ DEFERRED
+ """
+ Keep the existing payment schedule unchanged for the item (8.1.0+)
+ """
+ KEEP_EXISTING
+}
+
+"""
+Product-level subscription replacement parameters (Android)
+Used with setSubscriptionProductReplacementParams in BillingFlowParams.ProductDetailsParams
+Available in Google Play Billing Library 8.1.0+
+"""
+input SubscriptionProductReplacementParamsAndroid {
+ """
+ The old product ID that needs to be replaced
+ """
+ oldProductId: String!
+ """
+ The replacement mode for this product change
+ """
+ replacementMode: SubscriptionReplacementModeAndroid!
}
input AndroidSubscriptionOfferInput {
@@ -378,3 +437,154 @@ type UserChoiceBillingDetails {
"""
products: [String!]!
}
+
+# External Billing Programs (Google Play Billing Library 8.2.0+)
+"""
+Billing program types for external content links and external offers (Android)
+Available in Google Play Billing Library 8.2.0+
+"""
+enum BillingProgramAndroid {
+ """
+ Unspecified billing program. Do not use.
+ """
+ UNSPECIFIED
+ """
+ External Content Links program.
+ Allows linking to external content outside the app.
+ """
+ EXTERNAL_CONTENT_LINK
+ """
+ External Offers program.
+ Allows offering digital content purchases outside the app.
+ """
+ EXTERNAL_OFFER
+}
+
+"""
+Launch mode for external link flow (Android)
+Determines how the external URL is launched
+Available in Google Play Billing Library 8.2.0+
+"""
+enum ExternalLinkLaunchModeAndroid {
+ """
+ Unspecified launch mode. Do not use.
+ """
+ UNSPECIFIED
+ """
+ Play will launch the URL in an external browser or eligible app
+ """
+ LAUNCH_IN_EXTERNAL_BROWSER_OR_APP
+ """
+ Play will not launch the URL. The app handles launching the URL after Play returns control.
+ """
+ CALLER_WILL_LAUNCH_LINK
+}
+
+"""
+Link type for external link flow (Android)
+Specifies the type of external link destination
+Available in Google Play Billing Library 8.2.0+
+"""
+enum ExternalLinkTypeAndroid {
+ """
+ Unspecified link type. Do not use.
+ """
+ UNSPECIFIED
+ """
+ The link will direct users to a digital content offer
+ """
+ LINK_TO_DIGITAL_CONTENT_OFFER
+ """
+ The link will direct users to download an app
+ """
+ LINK_TO_APP_DOWNLOAD
+}
+
+"""
+Parameters for launching an external link (Android)
+Used with launchExternalLink to initiate external offer or app install flows
+Available in Google Play Billing Library 8.2.0+
+"""
+input LaunchExternalLinkParamsAndroid {
+ """
+ The billing program (EXTERNAL_CONTENT_LINK or EXTERNAL_OFFER)
+ """
+ billingProgram: BillingProgramAndroid!
+ """
+ The external link launch mode
+ """
+ launchMode: ExternalLinkLaunchModeAndroid!
+ """
+ The type of the external link
+ """
+ linkType: ExternalLinkTypeAndroid!
+ """
+ The URI where the content will be accessed from
+ """
+ linkUri: String!
+}
+
+"""
+Reporting details for transactions made outside of Google Play Billing (Android)
+Contains the external transaction token needed for reporting
+Available in Google Play Billing Library 8.2.0+
+"""
+type BillingProgramReportingDetailsAndroid {
+ """
+ The billing program that the reporting details are associated with
+ """
+ billingProgram: BillingProgramAndroid!
+ """
+ External transaction token used to report transactions made outside of Google Play Billing.
+ This token must be used when reporting the external transaction to Google.
+ """
+ externalTransactionToken: String!
+}
+
+"""
+Result of checking billing program availability (Android)
+Available in Google Play Billing Library 8.2.0+
+"""
+type BillingProgramAvailabilityResultAndroid {
+ """
+ Whether the billing program is available for the user
+ """
+ isAvailable: Boolean!
+ """
+ The billing program that was checked
+ """
+ billingProgram: BillingProgramAndroid!
+}
+
+# Deprecated External Offers APIs (Google Play Billing Library 8.2.0)
+# These APIs are deprecated in favor of the new BillingProgram APIs above
+
+"""
+External offer reporting details (Android)
+@deprecated Use BillingProgramReportingDetailsAndroid with createBillingProgramReportingDetailsAsync instead
+Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+"""
+type ExternalOfferReportingDetailsAndroid
+ @deprecated(
+ reason: "Use BillingProgramReportingDetailsAndroid with createBillingProgramReportingDetailsAsync instead"
+ ) {
+ """
+ External transaction token for reporting external offer transactions
+ """
+ externalTransactionToken: String!
+}
+
+"""
+External offer availability result (Android)
+@deprecated Use BillingProgramAvailabilityResultAndroid with isBillingProgramAvailableAsync instead
+Available in Google Play Billing Library 6.2.0+, deprecated in 8.2.0
+"""
+type ExternalOfferAvailabilityResultAndroid
+ @deprecated(
+ reason: "Use BillingProgramAvailabilityResultAndroid with isBillingProgramAvailableAsync instead"
+ ) {
+ """
+ Whether external offers are available for the user
+ """
+ isAvailable: Boolean!
+}