Use this page with AI Copy this into your coding assistant and add your request.
Read https://openiap.dev/docs/apis/fetch-products 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 → fetchProducts Retrieve in-app products, subscriptions, or a mixed result from the store by SKU.
iOS: Wraps Product.products(for:) (StoreKit 2). Fetches the localized price/title for each SKU. Unknown SKUs are simply omitted from the returned array — only transport failures (network, store unavailable, etc.) throw. Apple docs . Android: Calls BillingClient.queryProductDetailsAsync with the right ProductType (INAPP/SUBS) per request. Unknown SKUs return an empty list, not an error. Google docs . When type is 'all', Android queries both INAPP and SUBS product details and returns a mixed result that preserves product and subscription variants.
Requires an open connection
React Native / Expo: wait for the hook's connected flag, or call initConnection() and require a true result. Android does not connect implicitly; a disconnected call rejects with not-prepared. iOS can connect on demand, but the same gate keeps behavior consistent across platforms.
Note about request* APIs# Note
This note is about sibling APIs, not fetchProducts. fetchProducts itself is a regular promise-based call — its Promise<FetchProductsResult> return value is the canonical way to read the products you queried.
Reader pitfall to be aware of: the API in this library that does start with request (requestPurchase ) is event-based . Its return value is not the purchase result — listen via purchaseUpdatedListener / purchaseErrorListener instead. This is because Apple's purchase system is fundamentally event-based; see this issue comment .
Signature func fetchProducts(_ params: ProductRequest) async throws -> FetchProductsResultKotlin example suspend fun fetchProducts(params: ProductRequest): FetchProductsResultTypeScript example fetchProducts(params: ProductRequest): Promise<FetchProductsResult>
interface ProductRequest {
skus: string[];
type?: 'in-app' | 'subs' | 'all'; // Defaults to 'in-app'
}
// FetchProductsResult is the union returned by the canonical schema —
// the variant depends on the request `type`.
type FetchProductsResult =
| Product[]
| ProductSubscription[]
| ProductOrSubscription[]
| null;Dart example Future<List<T>> fetchProducts<T extends ProductCommon>({
required List<String> skus,
ProductQueryType type = ProductQueryType.InApp,
});C# (MAUI) example Task<FetchProductsResult> FetchProductsAsync(ProductRequest @params);
// FetchProductsResult is a sealed record union; pattern-match on the variant.
public abstract record FetchProductsResult;
public sealed record FetchProductsResultProducts(IReadOnlyList<Product>? Value) : FetchProductsResult;
public sealed record FetchProductsResultSubscriptions(IReadOnlyList<ProductSubscription>? Value) : FetchProductsResult;
public sealed record FetchProductsResultAll(IReadOnlyList<ProductOrSubscription>? Value) : FetchProductsResult;Kotlin (KMP) example suspend fun fetchProducts(params: ProductRequest): FetchProductsResultGDScript example # Returns Array[Product] for IN_APP, Array[ProductSubscription] for SUBS,
# or a mixed Array for ALL — typed as Array because GDScript can't express
# heterogeneous element types.
func fetch_products(request: ProductRequest) -> ArrayParameters# Pass a single ProductRequest object:
skus (required, string[]) — Product identifiers to fetch.type (optional, 'in-app' | 'subs' | 'all', default 'in-app') — Filter by product kind. Use 'all' to query both in one call.Returns# Promise<FetchProductsResult> — sealed union, discriminated by the request type:
Product[] (for type: 'in-app') — Array of one-time products. Empty array if none of the SKUs exist.ProductSubscription[] (for type: 'subs') — Array of subscription products with offer details.(Product | ProductSubscription)[] (for type: 'all') — Mixed array that preserves each item as either a product or a subscription. TypeScript-based wrappers expose this as a flat discriminated union; generated Kotlin, Dart, and C# schema handlers expose the same contract through their language-specific union wrappers.null (legacy) — Older schema branch retained for backwards compatibility.Fetch all product types# Use type: 'all' only when you intentionally want one request to return both in-app products and subscriptions. If type is omitted, OpenIAP defaults to in-app products.
let result = try await OpenIapModule.shared.fetchProducts(
ProductRequest(skus: ["com.app.coins_100", "com.app.monthly"], type: .all)
)
if case let .all(items) = result {
for item in items ?? [] {
switch item {
case let .product(product):
print("in-app product \(product.id)")
case let .productSubscription(subscription):
print("subscription \(subscription.id)")
}
}
}Kotlin example val result = openIapStore.fetchProducts(
ProductRequest(
skus = listOf("com.app.coins_100", "com.app.monthly"),
type = ProductQueryType.All,
)
)
if (result is FetchProductsResultAll) {
result.value.orEmpty().forEach { item ->
when (item) {
is ProductOrSubscription.ProductItem ->
println("in-app product " + item.value.id)
is ProductOrSubscription.ProductSubscriptionItem ->
println("subscription " + item.value.id)
}
}
}TypeScript example import { fetchProducts } from 'expo-iap';
const items = await fetchProducts({
skus: ['com.app.coins_100', 'com.app.monthly'],
type: 'all',
});
for (const item of items ?? []) {
if (item.type === 'subs') {
console.log('subscription', item.id, item.subscriptionOffers);
} else {
console.log('in-app product', item.id, item.displayPrice);
}
}Dart example final items = await FlutterInappPurchase.instance
.fetchProducts<ProductCommon>(
skus: ['com.app.coins_100', 'com.app.monthly'],
type: ProductQueryType.All,
);
for (final item in items) {
switch (item.type) {
case ProductType.Subs:
print('subscription ' + item.id);
case ProductType.InApp:
print('in-app product ' + item.id);
}
}C# (MAUI) example var result = await iap.FetchProductsAsync(new ProductRequest {
Skus = new[] { "com.app.coins_100", "com.app.monthly" },
Type = ProductQueryType.All,
});
if (result is FetchProductsResultAll allResult) {
foreach (var item in allResult.Value ?? Array.Empty<ProductOrSubscription>()) {
switch (item) {
case Product product:
Console.WriteLine(
quot;in-app product {product.Id}");
break;
case ProductSubscription subscription:
Console.WriteLine(
quot;subscription {subscription.Id}");
break;
}
}
}Kotlin (KMP) example val result = kmpIAP.fetchProducts(
ProductRequest(
skus = listOf("com.app.coins_100", "com.app.monthly"),
type = ProductQueryType.All,
)
)
if (result is FetchProductsResultAll) {
result.value.orEmpty().forEach { item ->
when (item) {
is ProductOrSubscription.ProductItem ->
println("in-app product " + item.value.id)
is ProductOrSubscription.ProductSubscriptionItem ->
println("subscription " + item.value.id)
}
}
}GDScript example var request = ProductRequest.new()
request.skus = ["com.app.coins_100", "com.app.monthly"]
request.type = ProductQueryType.ALL
var items = await iap.fetch_products(request)
for item in items:
if item.type == ProductType.SUBS:
print("subscription ", item.id)
else:
print("in-app product ", item.id)Example let products = try await OpenIapModule.shared.fetchProducts(
ProductRequest(skus: ["com.app.premium"], type: .inApp)
)Kotlin example val products = openIapStore.fetchProducts(
ProductRequest(skus = listOf("com.app.premium"), type = ProductQueryType.InApp)
)TypeScript example // expo-iap
import { fetchProducts } from 'expo-iap';
// Same API in react-native-iap:
// import { fetchProducts } from 'react-native-iap';
// Fetch one-time products
const products = await fetchProducts({
skus: ['com.app.coins_100', 'com.app.premium'],
type: 'in-app',
});
// Fetch subscriptions
const subscriptions = await fetchProducts({
skus: ['com.app.monthly', 'com.app.yearly'],
type: 'subs',
});
// --- Or via the useIAP() hook (also exported from react-native-iap) ---
// The hook exposes fetchProducts plus a reactive products array that is
// populated whenever fetchProducts resolves.
import { useEffect } from 'react';
import { FlatList, Text } from 'react-native';
import { useIAP } from 'expo-iap';
function ProductList() {
const { connected, products, fetchProducts } = useIAP();
useEffect(() => {
if (!connected) return;
void fetchProducts({
skus: ['com.app.coins_100', 'com.app.premium'],
type: 'in-app',
}).catch((error) => console.warn('Product fetch failed:', error));
}, [connected, fetchProducts]);
return (
<FlatList
data={products}
keyExtractor={(product) => product.id}
renderItem={({ item }) => (
<Text>{item.title} — {item.displayPrice}</Text>
)}
/>
);
}Dart example final products = await FlutterInappPurchase.instance.fetchProducts<Product>(
skus: ['com.app.premium'],
type: ProductQueryType.InApp,
);
for (final product in products) {
print(product.title + ': ' + product.displayPrice);
}C# (MAUI) example using OpenIap;
using OpenIap.Maui;
var iap = (QueryResolver)OpenIapClient.Instance;
var result = await iap.FetchProductsAsync(new ProductRequest {
Skus = new[] { "com.app.coins_100", "com.app.premium" },
Type = ProductQueryType.InApp,
});
// Pattern-match the sealed result union
var products = result switch {
FetchProductsResultProducts r => r.Value ?? Array.Empty<Product>(),
_ => Array.Empty<Product>(),
};Kotlin (KMP) example import io.github.hyochan.kmpiap.KmpIAP
import io.github.hyochan.kmpiap.fetchProducts // DSL extension
val kmpIAP = KmpIAP()
// 1) Builder pattern — pass a ProductRequest data class directly.
// Returns the sealed FetchProductsResult union; unwrap by variant.
val result = kmpIAP.fetchProducts(
ProductRequest(
skus = listOf("com.app.premium"),
type = ProductQueryType.InApp,
)
)
// 2) DSL pattern — trailing-lambda builder.
// Returns List<Product> directly (already unwrapped).
val products = kmpIAP.fetchProducts {
skus = listOf("com.app.premium")
type = ProductQueryType.InApp
}GDScript example var request = ProductRequest.new()
request.skus = ["com.app.coins_100", "com.app.premium"]
request.type = ProductQueryType.IN_APP
var products = await iap.fetch_products(request)See: Product , ProductSubscription