Use this page with AI Copy this into your coding assistant and add your request.
Read https://openiap.dev/docs/events/ios/promoted-product-listener-ios 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 → iOS promotedProductListenerIOSFired when a user clicks on a promoted in-app purchase in the App Store.
Listener Setup func promotedProductListenerIOS(
_ listener: @escaping (String) -> Void
) -> SubscriptionKotlin example // iOS only - not available on AndroidTypeScript example promotedProductListenerIOS(
listener: (product: Product) => void
): EventSubscriptionDart example // iOS only
Stream<String?> get purchasePromoted;C# (MAUI) example using OpenIap;
using OpenIap.Maui;
// Emits on iOS; it stays silent on Android.
IObservable<string> promotedProducts = OpenIapClient.Instance.PromotedProductIOS;Kotlin (KMP) example // Emits on iOS targets; it stays silent on Android.
val promotedProductListener: Flow<String?>GDScript example # iOS only
signal promoted_product_ios(product_id: String)Registers a listener for App Store promoted product events. OpenIAP uses PurchaseIntent.intents on iOS 16.4+ and the StoreKit 1 observer only on iOS 15–16.3, so both mechanisms never run together.
import OpenIap
let subscription = OpenIapModule.shared.promotedProductListenerIOS { productId in
Task {
print("Promoted product tapped: \(productId)")
do {
let result = try await OpenIapModule.shared.fetchProducts(
ProductRequest(skus: [productId], type: .all)
)
guard case let .all(items) = result,
let item = items?.first else { return }
switch item {
case let .product(product):
guard await showPurchaseConfirmation(product) else { return }
try await OpenIapModule.shared.requestPurchase(
RequestPurchaseProps(
request: .purchase(
RequestPurchasePropsByPlatforms(
apple: RequestPurchaseIosProps(sku: productId)
)
)
)
)
case let .productSubscription(subscription):
guard await showPurchaseConfirmation(subscription) else { return }
try await OpenIapModule.shared.requestPurchase(
RequestPurchaseProps(
request: .subscription(
RequestSubscriptionPropsByPlatforms(
apple: RequestSubscriptionIosProps(sku: productId)
)
)
)
)
}
} catch {
print("Promoted purchase failed: \(error.localizedDescription)")
}
}
}
// Keep the token in the owning object, then call this on teardown.
func stopListeningForPromotedProducts() {
OpenIapModule.shared.removeListener(subscription)
}TypeScript example import {
fetchProducts,
promotedProductListenerIOS,
requestPurchase
} from 'expo-iap';
const subscription = promotedProductListenerIOS(async (product) => {
try {
const productId = product.id;
console.log('Promoted product tapped:', productId);
// Refetch as "all" because the listener's legacy Product payload does not
// distinguish promoted subscriptions at the type level.
const items = (await fetchProducts({ skus: [productId], type: 'all' })) ?? [];
const item = items.find((candidate) => candidate.id === productId);
if (!item) return;
const confirmed = await showPurchaseConfirmation(item);
if (confirmed) {
if (item.type === 'subs') {
await requestPurchase({
request: { apple: { sku: productId } },
type: 'subs'
});
} else {
await requestPurchase({
request: { apple: { sku: productId } },
type: 'in-app'
});
}
}
} catch (error) {
console.error('Promoted purchase failed:', error);
}
});
// Cleanup when done
subscription.remove();Dart example import 'dart:async';
import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
// iOS only - will not fire on Android
final iap = FlutterInappPurchase.instance;
Future<void> handlePromotedProduct(String? productId) async {
if (productId == null) return;
try {
print('Promoted product tapped: $productId');
final products = await iap.fetchProducts<ProductCommon>(
skus: [productId],
type: ProductQueryType.All,
);
if (products.isNotEmpty) {
final confirmed = await showPurchaseConfirmation(products.first);
if (confirmed) {
if (products.first.type == ProductType.Subs) {
await iap.requestPurchase(
RequestPurchaseProps.subs((
apple: RequestSubscriptionIosProps(sku: productId),
google: null,
)),
);
} else {
await iap.requestPurchase(
RequestPurchaseProps.inApp((
apple: RequestPurchaseIosProps(sku: productId),
google: null,
)),
);
}
}
}
} catch (error) {
print('Promoted purchase failed: $error');
}
}
final subscription = iap.purchasePromoted.listen((productId) {
unawaited(handlePromotedProduct(productId));
});
// Cleanup when done
subscription.cancel();C# (MAUI) example using OpenIap;
using OpenIap.Maui;
var iap = OpenIapClient.Instance;
var query = (QueryResolver)iap;
var mutate = (MutationResolver)iap;
async Task HandlePromotedProductAsync(string productId)
{
try
{
var result = await query.FetchProductsAsync(new ProductRequest
{
Skus = new[] { productId },
Type = ProductQueryType.All,
});
var product = (result as FetchProductsResultAll)?.Value?.FirstOrDefault();
if (product is null || !await ShowPurchaseConfirmationAsync(product)) return;
var props = product is ProductSubscription
? new RequestPurchaseProps
{
RequestSubscription = new RequestSubscriptionPropsByPlatforms
{
Apple = new RequestSubscriptionIosProps { Sku = productId },
},
Type = ProductQueryType.Subs,
}
: new RequestPurchaseProps
{
RequestPurchase = new RequestPurchasePropsByPlatforms
{
Apple = new RequestPurchaseIosProps { Sku = productId },
},
Type = ProductQueryType.InApp,
};
await mutate.RequestPurchaseAsync(props);
}
catch (Exception error)
{
Console.Error.WriteLine(
quot;Promoted purchase failed: {error.Message}");
}
}
using var subscription = iap.PromotedProductIOS.Subscribe(productId =>
{
_ = HandlePromotedProductAsync(productId);
});Kotlin (KMP) example import io.github.hyochan.kmpiap.KmpIAP
import io.github.hyochan.kmpiap.openiap.*
import kotlinx.coroutines.CancellationException
val iap = KmpIAP()
scope.launch {
iap.promotedProductListener.collect { productId ->
productId ?: return@collect
try {
val result = iap.fetchProducts(
ProductRequest(skus = listOf(productId), type = ProductQueryType.All)
)
val item = (result as? FetchProductsResultAll)
?.value
?.firstOrNull()
when (item) {
is ProductOrSubscription.ProductItem -> {
if (!showPurchaseConfirmation(item.value)) return@collect
iap.requestPurchase(
RequestPurchaseProps(
request = RequestPurchaseProps.Request.Purchase(
RequestPurchasePropsByPlatforms(
apple = RequestPurchaseIosProps(sku = productId)
)
),
type = ProductQueryType.InApp
)
)
}
is ProductOrSubscription.ProductSubscriptionItem -> {
if (!showPurchaseConfirmation(item.value)) return@collect
iap.requestPurchase(
RequestPurchaseProps(
request = RequestPurchaseProps.Request.Subscription(
RequestSubscriptionPropsByPlatforms(
apple = RequestSubscriptionIosProps(sku = productId)
)
),
type = ProductQueryType.Subs
)
)
}
null -> Unit
}
} catch (error: CancellationException) {
throw error
} catch (error: Exception) {
println("Promoted purchase failed: ${error.message}")
}
}
}GDScript example const Types = preload("res://addons/godot-iap/types.gd")
func _ready() -> void:
GodotIapPlugin.promoted_product_ios.connect(_on_promoted_product_ios)
func _on_promoted_product_ios(product_id: String) -> void:
var request := Types.ProductRequest.new()
var skus: Array[String] = [product_id]
request.skus = skus
request.type = Types.ProductQueryType.ALL
var items = await GodotIapPlugin.fetch_products(request)
if items.is_empty():
return
var item = items[0]
if not await show_purchase_confirmation(item):
return
var props := Types.RequestPurchaseProps.new()
if item is Types.ProductSubscriptionIOS:
props.request_subscription = Types.RequestSubscriptionPropsByPlatforms.new()
props.request_subscription.apple = Types.RequestSubscriptionIosProps.new()
props.request_subscription.apple.sku = product_id
props.type = Types.ProductQueryType.SUBS
else:
props.request = Types.RequestPurchasePropsByPlatforms.new()
props.request.apple = Types.RequestPurchaseIosProps.new()
props.request.apple.sku = product_id
props.type = Types.ProductQueryType.IN_APP
# Results arrive through purchase_updated or purchase_error.
GodotIapPlugin.request_purchase(props)Handling Promoted Products Receive product SKU via listener Fetch product details using fetchProducts Display product information to user Call requestPurchase with the received SKU if user confirms Also check getPromotedProductIOS on app launch for pending promoted products.
Note
In StoreKit 2, promoted products are purchased through the standard
requestPurchase() flow after the app receives or restores the promoted product. If the purchase intent contains an externally redeemed win-back offer, OpenIAP automatically carries it into the next matching purchase unless that request supplies an explicit win-back or promotional offer.