Use this page with AI Copy this into your coding assistant and add your request.
Read https://openiap.dev/docs/types/alternative-billing-types and https://openiap.dev/llms.txt. Follow the reading instructions, detailed reference, and linked guides relevant to my task before making changes.
Inspect my existing project and reuse its framework and conventions. Ask me for missing product decisions. Implement the requested behavior and run the applicable checks.
Show the working result, the commands and actual test results, and any remaining limitations. Keep your explanation brief.
My request: [describe what customers should be able to do]See an example request → Billing Program Configuration Android Billing Programs# Configure Google Play billing programs through InitConnectionConfig.enableBillingProgramAndroid.
These options are Android only . See the current Google Play requirements in the Google alternative billing documentation .
Native references: Google · Alternative billing · Google · User Choice Billing
InitConnectionConfig# Configuration options for initConnection() :
Name Summary enableBillingProgramAndroid(Recommended) Enable a specific billing program during connection. Use USER_CHOICE_BILLING for user choice, EXTERNAL_OFFER for alternative only, or EXTERNAL_PAYMENTS for Japan external payments (8.3.0+). Use BILLING_CHOICE for Billing Choice (OpenIAP 2.1.0 / openiap-google 2.3.0; Play Billing 9.1.0+).billingChoiceScreenTypeAndroidBilling Choice renderer in OpenIAP 2.1.0 / openiap-google 2.3.0 (Play Billing 9.1.0+). Defaults to GOOGLE_RENDERED. Set DEVELOPER_RENDERED when your app renders the choice screen; this controls whether OpenIAP registers Play's developer-provided billing listener.
Basic Usage (Recommended)# // iOS uses standard StoreKit billing
// Alternative billing is Android-only
let connected = try await OpenIapModule.shared.initConnection()
precondition(connected, "Store connection failed")Kotlin example // Choose one configuration. End the current connection before changing programs.
val config = InitConnectionConfig(
// Also supported: UserChoiceBilling, ExternalPayments, or BillingChoice.
enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer
// For developer-rendered Billing Choice, also set billingChoiceScreenTypeAndroid.
)
// Pass no config instead for standard billing.
val connected = openIapStore.initConnection(config)
check(connected) { "Store connection failed" }TypeScript example // Choose one configuration. End the current connection before changing programs.
const config = {
// Also supported: 'user-choice-billing', 'external-payments', or 'billing-choice'.
enableBillingProgramAndroid: 'external-offer',
// For developer-rendered Billing Choice, also set:
// billingChoiceScreenTypeAndroid: 'developer-rendered',
} as const;
// Pass no config instead for standard billing.
const connected = await initConnection(config);
if (!connected) throw new Error('Store connection failed');Dart example // Choose one configuration. End the current connection before changing programs.
// Also supported: UserChoiceBilling, ExternalPayments, or BillingChoice.
final connected = await FlutterInappPurchase.instance.initConnection(
enableBillingProgramAndroid: BillingProgramAndroid.ExternalOffer,
);
// Omit the named arguments instead for standard billing.
if (!connected) throw StateError('Store connection failed');C# (MAUI) example using OpenIap;
using OpenIap.Maui;
// Choose one configuration. End the current connection before changing programs.
var config = new InitConnectionConfig
{
// Also supported: UserChoiceBilling, ExternalPayments, or BillingChoice.
EnableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer,
// For developer-rendered Billing Choice, also set BillingChoiceScreenTypeAndroid.
};
// Pass no config instead for standard billing.
var connected = await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync(config);
if (!connected) throw new InvalidOperationException("Store connection failed");GDScript example # Choose one configuration. End the current connection before changing programs.
var config = InitConnectionConfig.new()
# Also supported: USER_CHOICE_BILLING, EXTERNAL_PAYMENTS, or BILLING_CHOICE.
config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER
# For developer-rendered Billing Choice, also set billing_choice_screen_type_android.
# Pass no config instead for standard billing.
var success = await iap.init_connection(config)
if not success:
push_error("Store connection failed")User Choice Billing Complete Example# With User Choice Billing (7.0+), users see a dialog to choose between Google Play or your alternative payment. Handle both paths:
import dev.hyo.openiap.*
import dev.hyo.openiap.listener.OpenIapUserChoiceBillingListener
import dev.hyo.openiap.store.OpenIapStore
val iapStore = OpenIapStore(context)
// Step 1: Set up listener for when user selects alternative billing
val userChoiceListener = object : OpenIapUserChoiceBillingListener {
override fun onUserChoiceBilling(details: UserChoiceBillingDetails) {
Log.d("IAP", "User chose alternative billing")
for (productId in details.products) {
Log.d("IAP", "Product: $productId")
}
Log.d("IAP", "External transaction token received; send it to your backend without logging it.")
// Process payment with your backend using the token
lifecycleScope.launch {
try {
val paymentResult = yourBackend.processPayment(
products = details.products,
token = details.externalTransactionToken
)
if (paymentResult.success) {
grantUserAccess()
}
} catch (e: Exception) {
Log.e("IAP", "Alternative billing failed: ${e.message}")
}
}
}
}
iapStore.addUserChoiceBillingListener(userChoiceListener)
// Step 2: Initialize with user choice billing (recommended)
val connected = iapStore.initConnection(
InitConnectionConfig(
enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling
)
)
check(connected) { "Store connection failed" }
// Step 3: Fetch products and purchase as normal
val products = iapStore.fetchProducts(
ProductRequest(
skus = listOf("premium_subscription"),
type = ProductQueryType.Subs
)
)
// Step 4: Request purchase - dialog will show both options
iapStore.setActivity(activity)
iapStore.requestPurchase(
RequestPurchaseProps(
request = RequestPurchaseProps.Request.Subscription(
RequestSubscriptionPropsByPlatforms(
google = RequestSubscriptionAndroidProps(
skus = listOf("premium_subscription")
)
)
),
type = ProductQueryType.Subs
)
)
// If user selects Google Play → onPurchaseSuccess fires
// If user selects alternative → OpenIapUserChoiceBillingListener fires
// Call when the owning lifecycle ends.
fun disposeUserChoiceListener() {
iapStore.removeUserChoiceBillingListener(userChoiceListener)
}TypeScript example import {
initConnection,
userChoiceBillingListenerAndroid,
fetchProducts,
requestPurchase,
} from 'expo-iap';
// Step 1: Set up listener for when user selects alternative billing
const userChoiceSubscription = userChoiceBillingListenerAndroid((details) => {
void (async () => {
console.log('User chose alternative billing');
const products = details.products ?? [];
for (const productId of products) {
console.log('Product:', productId);
}
console.log('External transaction token received; send it to your backend without logging it.');
// Process payment with your backend using the token
const paymentResult = await yourBackend.processPayment({
products,
token: details.externalTransactionToken,
});
if (paymentResult.success) {
grantUserAccess();
}
})().catch((error) => console.warn('Alternative payment failed:', error));
});
// Step 2: Initialize with user choice billing (recommended)
const connected = await initConnection({
enableBillingProgramAndroid: 'user-choice-billing',
});
if (!connected) throw new Error('Store connection failed');
// Step 3: Fetch products and purchase as normal
const products = await fetchProducts({
skus: ['premium_subscription'],
type: 'subs',
});
// Step 4: Request purchase - dialog will show both options
await requestPurchase({
request: {
google: { skus: ['premium_subscription'] },
},
type: 'subs',
});
// If user selects Google Play → purchaseUpdatedListener fires
// If user selects alternative → userChoiceBillingListenerAndroid fires
// Call from your screen/component cleanup hook.
const disposeUserChoiceListener = () => userChoiceSubscription.remove();Dart example import 'dart:async';
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
Future<void> handleUserChoiceBilling(UserChoiceBillingDetails details) async {
print('User chose alternative billing');
for (final productId in details.products) {
print('Product: $productId');
}
print('External transaction token received; send it to your backend without logging it.');
final paymentResult = await yourBackend.processPayment(
products: details.products,
token: details.externalTransactionToken,
);
if (paymentResult.success) {
grantUserAccess();
}
}
// Step 1: Set up listener for when user selects alternative billing
final userChoiceSubscription = FlutterInappPurchase.instance.userChoiceBillingAndroid
.listen((details) {
unawaited(handleUserChoiceBilling(details).catchError(
(Object error) => print('Alternative billing failed: $error'),
));
});
// Step 2: Initialize with user choice billing (recommended)
final connected = await FlutterInappPurchase.instance.initConnection(
enableBillingProgramAndroid: BillingProgramAndroid.UserChoiceBilling,
);
if (!connected) throw StateError('Store connection failed');
// Step 3: Fetch products and purchase as normal
final products = await FlutterInappPurchase.instance
.fetchProducts<ProductSubscription>(
skus: ['premium_subscription'],
type: ProductQueryType.Subs,
);
// Step 4: Request purchase - dialog will show both options
await FlutterInappPurchase.instance.requestPurchase(
RequestPurchaseProps.subs((
apple: null,
google: RequestSubscriptionAndroidProps(
skus: ['premium_subscription'],
),
)),
);
// If user selects Google Play → purchaseUpdatedListener fires
// If user selects alternative → userChoiceBillingAndroid fires
// Call from your State.dispose() method.
Future<void> disposeUserChoiceListener() => userChoiceSubscription.cancel();C# (MAUI) example using OpenIap;
using OpenIap.Maui;
async Task HandleUserChoiceBillingSafelyAsync(UserChoiceBillingDetails details)
{
try
{
Console.WriteLine("User chose alternative billing");
Console.WriteLine(
quot;Products: {string.Join(", ", details.Products)}");
Console.WriteLine("External transaction token received; send it to your backend without logging it.");
var paymentResult = await ProcessPaymentWithBackendAsync(
products: details.Products,
token: details.ExternalTransactionToken);
if (paymentResult.Success)
{
await GrantUserAccessAsync();
}
}
catch (Exception error)
{
Console.WriteLine(
quot;Alternative billing failed: {error.Message}");
}
}
// Step 1: Set up listener for when user selects alternative billing.
var userChoiceSubscription = OpenIapClient.Instance.UserChoiceBillingAndroid.Subscribe(
details => _ = HandleUserChoiceBillingSafelyAsync(details));
// Step 2: Initialize with user choice billing (recommended).
var connected = await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync(new InitConnectionConfig
{
EnableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling,
});
if (!connected) throw new InvalidOperationException("Store connection failed");
// Step 3: Fetch products and purchase as normal.
await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync(new ProductRequest
{
Skus = new[] { "premium_subscription" },
Type = ProductQueryType.Subs,
});
// Step 4: Request purchase - dialog will show both options.
await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new RequestPurchaseProps
{
Type = ProductQueryType.Subs,
RequestSubscription = new RequestSubscriptionPropsByPlatforms
{
Google = new RequestSubscriptionAndroidProps
{
Skus = new[] { "premium_subscription" },
},
},
});
// If user selects Google Play, purchase updated fires.
// If user selects alternative, UserChoiceBillingAndroid fires.
// Call when the owning lifecycle ends.
void DisposeUserChoiceListener() => userChoiceSubscription.Dispose();GDScript example # Step 1: Set up listener for when user selects alternative billing
func _on_user_choice_billing(details: UserChoiceBillingDetails):
print("User chose alternative billing")
print("Products: %s" % str(details.products))
print("External transaction token received; send it to your backend without logging it.")
# Process payment with your backend using the token
var payment_result = await your_backend.process_payment(
details.products,
details.external_transaction_token
)
if payment_result.success:
grant_user_access()
iap.user_choice_billing_android.connect(_on_user_choice_billing)
# Step 2: Initialize with user choice billing (recommended)
var config = InitConnectionConfig.new()
config.enable_billing_program_android = BillingProgramAndroid.USER_CHOICE_BILLING
var connected = await iap.init_connection(config)
if not connected:
push_error("Store connection failed")
return
# Step 3: Fetch products and purchase as normal
var request = ProductRequest.new()
request.skus = ["premium_subscription"]
request.type = ProductQueryType.SUBS
var products = await iap.fetch_products(request)
# Step 4: Request purchase - dialog will show both options
var props = RequestPurchaseProps.new()
props.request_subscription = RequestSubscriptionPropsByPlatforms.new()
props.request_subscription.google = RequestSubscriptionAndroidProps.new()
props.request_subscription.google.skus = ["premium_subscription"]
props.type = ProductQueryType.SUBS
await iap.request_purchase(props)
# If user selects Google Play → purchase_updated signal fires
# If user selects alternative → user_choice_billing_android signal fires
# Cleanup when the owning node exits
func _exit_tree() -> void:
if iap.user_choice_billing_android.is_connected(_on_user_choice_billing):
iap.user_choice_billing_android.disconnect(_on_user_choice_billing)Alternative Billing Only Complete Example# With External Offer mode (replaces Alternative Only), all purchases go through your alternative payment system. Use the Billing Programs API available with Play Billing 8.2.1+:
import dev.hyo.openiap.*
import dev.hyo.openiap.store.OpenIapStore
val iapStore = OpenIapStore(context)
// Step 1: Initialize with external offer (recommended)
val connected = iapStore.initConnection(
InitConnectionConfig(
enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer
)
)
check(connected) { "Store connection failed" }
// Step 2: Check whether External Offer is available for this user.
val availability = iapStore.isBillingProgramAvailable(
BillingProgramAndroid.ExternalOffer
)
if (!availability.isAvailable) {
Log.w("IAP", "External Offer is not available in this region")
return
}
// Step 3: Fetch products (still needed to show prices)
val products = iapStore.fetchProducts(
ProductRequest(
skus = listOf("premium_subscription"),
type = ProductQueryType.Subs
)
)
val product = (products as? FetchProductsResultSubscriptions)
?.value
?.firstOrNull()
?: return
// Step 4: Create reporting details immediately before redirecting.
val reportingDetails = iapStore.createBillingProgramReportingDetails(
BillingProgramAndroid.ExternalOffer
)
// Step 5: Play shows the disclosure and launches the checkout URL.
val launched = iapStore.launchExternalLink(
activity,
LaunchExternalLinkParamsAndroid(
billingProgram = BillingProgramAndroid.ExternalOffer,
launchMode = ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp,
linkType = ExternalLinkTypeAndroid.LinkToDigitalContentOffer,
linkUri = "https://your-payment-site.com/checkout"
)
)
if (!launched) return
// Step 6: This helper must wait for and verify checkout completion; a true
// launch result alone is not proof of purchase. Then report from your backend.
lifecycleScope.launch {
val paymentResult = yourBackend.completeAndReportExternalPurchase(
productId = product.id,
externalTransactionToken = reportingDetails.externalTransactionToken,
userId = currentUserId
)
if (paymentResult.success) {
grantUserAccess()
}
}TypeScript example import {
initConnection,
fetchProducts,
isBillingProgramAvailableAndroid,
createBillingProgramReportingDetailsAndroid,
launchExternalLinkAndroid,
} from 'expo-iap';
// Step 1: Initialize with external offer (recommended)
const connected = await initConnection({
enableBillingProgramAndroid: 'external-offer',
});
if (!connected) throw new Error('Store connection failed');
// Step 2: Check if alternative billing is available
const availability = await isBillingProgramAvailableAndroid('external-offer');
if (!availability.isAvailable) {
console.log('External Offer is not available in this region');
return;
}
// Step 3: Fetch products (still needed to show prices)
const products = await fetchProducts({
skus: ['premium_subscription'],
type: 'subs',
});
// Step 4: Create reporting details immediately before redirecting.
const reportingDetails =
await createBillingProgramReportingDetailsAndroid('external-offer');
// Step 5: Play shows the disclosure and launches the checkout URL.
const launched = await launchExternalLinkAndroid({
billingProgram: 'external-offer',
launchMode: 'launch-in-external-browser-or-app',
linkType: 'link-to-digital-content-offer',
linkUri: 'https://your-payment-site.com/checkout',
});
if (!launched) return;
// Step 6: This helper must wait for and verify checkout completion; a true
// launch result alone is not proof of purchase. Then report within 24 hours.
const paymentResult = await yourBackend.completeAndReportExternalPurchase({
productId: products[0].id,
externalTransactionToken: reportingDetails.externalTransactionToken,
userId: currentUserId,
});
if (paymentResult.success) {
grantUserAccess();
}Dart example import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
final iap = FlutterInappPurchase.instance;
// Step 1: Initialize with external offer (recommended)
final connected = await iap.initConnection(
enableBillingProgramAndroid: BillingProgramAndroid.ExternalOffer,
);
if (!connected) throw StateError('Store connection failed');
// Step 2: Check whether External Offer is available for this user.
final availability = await iap.isBillingProgramAvailableAndroid(
BillingProgramAndroid.ExternalOffer,
);
if (!availability.isAvailable) {
print('External Offer is not available in this region');
return;
}
// Step 3: Fetch products (still needed to show prices)
final products = await iap.fetchProducts<ProductSubscription>(
skus: ['premium_subscription'],
type: ProductQueryType.Subs,
);
// Step 4: Create reporting details immediately before redirecting.
final reportingDetails = await iap.createBillingProgramReportingDetailsAndroid(
program: BillingProgramAndroid.ExternalOffer,
);
// Step 5: Play shows the disclosure and launches the checkout URL.
final launched = await iap.launchExternalLinkAndroid(
billingProgram: BillingProgramAndroid.ExternalOffer,
launchMode: ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp,
linkType: ExternalLinkTypeAndroid.LinkToDigitalContentOffer,
linkUri: 'https://your-payment-site.com/checkout',
);
if (!launched) return;
// Step 6: This helper must wait for and verify checkout completion; a true
// launch result alone is not proof of purchase. Then report from your backend.
final paymentResult = await yourBackend.completeAndReportExternalPurchase(
productId: products.first.id,
externalTransactionToken: reportingDetails.externalTransactionToken,
userId: currentUserId,
);
if (paymentResult.success) {
grantUserAccess();
}C# (MAUI) example using OpenIap;
using OpenIap.Maui;
using System.Linq;
// Step 1: Initialize with external offer (recommended).
var connected = await ((MutationResolver)OpenIapClient.Instance).InitConnectionAsync(new InitConnectionConfig
{
EnableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer,
});
if (!connected) throw new InvalidOperationException("Store connection failed");
var mutate = (MutationResolver)OpenIapClient.Instance;
// Step 2: Check whether External Offer is available for this user.
var availability = await mutate.IsBillingProgramAvailableAndroidAsync(
BillingProgramAndroid.ExternalOffer);
if (!availability.IsAvailable)
{
Console.WriteLine("External Offer is not available in this region");
return;
}
// Step 3: Fetch products (still needed to show prices).
var result = await ((QueryResolver)OpenIapClient.Instance).FetchProductsAsync(new ProductRequest
{
Skus = new[] { "premium_subscription" },
Type = ProductQueryType.Subs,
});
var product = result is FetchProductsResultSubscriptions subscriptions
? subscriptions.Value?.OfType<ProductSubscriptionAndroid>()?.FirstOrDefault()
: null;
if (product is null) return;
// Step 4: Create reporting details immediately before redirecting.
var reportingDetails = await mutate.CreateBillingProgramReportingDetailsAndroidAsync(
BillingProgramAndroid.ExternalOffer);
// Step 5: Play shows the disclosure and launches the checkout URL.
var launched = await mutate.LaunchExternalLinkAndroidAsync(
new LaunchExternalLinkParamsAndroid
{
BillingProgram = BillingProgramAndroid.ExternalOffer,
LaunchMode = ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp,
LinkType = ExternalLinkTypeAndroid.LinkToDigitalContentOffer,
LinkUri = "https://your-payment-site.com/checkout",
});
if (!launched) return;
// Step 6: This helper must wait for and verify checkout completion; a true
// launch result alone is not proof of purchase. Then report from your backend.
var paymentResult = await CompleteAndReportExternalPurchaseAsync(
productId: product.Id,
externalTransactionToken: reportingDetails.ExternalTransactionToken,
userId: currentUserId);
if (paymentResult.Success)
{
await GrantUserAccessAsync();
}GDScript example # Step 1: Initialize with external offer (recommended)
var config = InitConnectionConfig.new()
config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER
var connected = await iap.init_connection(config)
if not connected:
push_error("Store connection failed")
return
# Step 2: Check whether External Offer is available for this user.
var availability = await iap.is_billing_program_available_android(
BillingProgramAndroid.EXTERNAL_OFFER
)
if not availability.is_available:
print("External Offer is not available in this region")
return
# Step 3: Fetch products (still needed to show prices)
var request = ProductRequest.new()
request.skus = ["premium_subscription"]
request.type = ProductQueryType.SUBS
var products = await iap.fetch_products(request)
# Step 4: Create reporting details immediately before redirecting.
var reporting_details = await iap.create_billing_program_reporting_details_android(
BillingProgramAndroid.EXTERNAL_OFFER
)
# Step 5: Play shows the disclosure and launches the checkout URL.
var params = LaunchExternalLinkParamsAndroid.new()
params.billing_program = BillingProgramAndroid.EXTERNAL_OFFER
params.launch_mode = ExternalLinkLaunchModeAndroid.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP
params.link_type = ExternalLinkTypeAndroid.LINK_TO_DIGITAL_CONTENT_OFFER
params.link_uri = "https://your-payment-site.com/checkout"
var launched = await iap.launch_external_link_android(params)
if not launched:
return
# Step 6: This helper must wait for and verify checkout completion; a true
# launch result alone is not proof of purchase. Then report from your backend.
var payment_result = await your_backend.complete_and_report_external_purchase(
products[0].id,
reporting_details.external_transaction_token,
current_user_id
)
if payment_result.success:
grant_user_access()Reporting Requirement: For both User Choice and Alternative Only modes, you must report completed transactions to Google Play within 24 hours using the Google Play Developer API. Failure to report may result in account suspension.