+ {/* Single disclosure button — both halves do the same thing, so
+ collapsing into one element keeps the keyboard tab order at one
+ stop per group instead of two redundant ones. */}
+
+
- Step 1: Check if alternative billing is available for
- this user/device.
-
- {`// Returns true if available, false otherwise
-// Throws OpenIapError.NotPrepared if billing client not ready
-suspend fun checkAlternativeBillingAvailability(): Boolean`}
-
-
- showAlternativeBillingDialogAndroid
-
-
- Step 2: Show alternative billing information dialog
- to the user. Must be called before processing payment
- in your payment system.
-
- {`// Returns true if user accepted, false if user canceled
-// Throws OpenIapError.NotPrepared if billing client not ready
-suspend fun showAlternativeBillingDialog(): Boolean`}
-
-
- createAlternativeBillingTokenAndroid
-
-
- Step 3: Create external transaction token for Google
- Play reporting. Must be called after successful
- payment in your payment system.
-
- {`// Token must be reported to Google Play backend within 24 hours
-// Returns token string, or null if creation failed
-suspend fun createAlternativeBillingToken(): String?`}
-
-
-
-
- Alternative Billing Flow Example
-
-
- {{
- typescript: (
- {`import {
- checkAlternativeBillingAvailabilityAndroid,
- showAlternativeBillingDialogAndroid,
- createAlternativeBillingTokenAndroid,
-} from 'expo-iap';
-
-async function handleAlternativePurchase() {
- // Step 1: Check availability
- const isAvailable = await checkAlternativeBillingAvailabilityAndroid();
- if (!isAvailable) {
- // Fall back to standard billing
- return;
- }
-
- // Step 2: Show dialog to user
- const userAccepted = await showAlternativeBillingDialogAndroid();
- if (!userAccepted) {
- // User canceled
- return;
- }
-
- // Process payment in your payment system
- const paymentSuccess = await processPaymentInYourSystem();
-
- if (paymentSuccess) {
- // Step 3: Create token and report to Google Play
- const token = await createAlternativeBillingTokenAndroid();
-
- if (token) {
- // Report token to Google Play backend within 24 hours
- await reportTokenToGooglePlay(token);
- }
- }
-}`}
- ),
- kotlin: (
- {`// Step 1: Check availability
-val isAvailable = openIapStore.checkAlternativeBillingAvailability()
-if (!isAvailable) {
- // Fall back to standard billing
- return
-}
-
-// Step 2: Show dialog to user
-val userAccepted = openIapStore.showAlternativeBillingDialog()
-if (!userAccepted) {
- // User canceled
- return
-}
-
-// Process payment in your payment system
-val paymentSuccess = processPaymentInYourSystem()
-
-if (paymentSuccess) {
- // Step 3: Create token and report to Google Play
- val token = openIapStore.createAlternativeBillingToken()
-
- token?.let {
- // Report token to Google Play backend within 24 hours
- reportTokenToGooglePlay(it)
- }
-}`}
- ),
- dart: (
- {`// Step 1: Check availability
-final isAvailable = await FlutterInappPurchase.instance
- .checkAlternativeBillingAvailabilityAndroid();
-if (!isAvailable) {
- return; // Fall back to standard billing
-}
-
-// Step 2: Show dialog to user
-final userAccepted = await FlutterInappPurchase.instance
- .showAlternativeBillingDialogAndroid();
-if (!userAccepted) {
- return; // User canceled
-}
-
-// Process payment in your payment system
-final paymentSuccess = await processPaymentInYourSystem();
-
-if (paymentSuccess) {
- // Step 3: Create token and report to Google Play
- final token = await FlutterInappPurchase.instance
- .createAlternativeBillingTokenAndroid();
-
- if (token != null) {
- await reportTokenToGooglePlay(token);
- }
-}`}
- ),
- gdscript: (
- {`func handle_alternative_purchase():
- # Step 1: Check availability
- var is_available = await iap.check_alternative_billing_availability_android()
- if not is_available:
- # Fall back to standard billing
- return
-
- # Step 2: Show dialog to user
- var user_accepted = await iap.show_alternative_billing_dialog_android()
- if not user_accepted:
- # User canceled
- return
-
- # Process payment in your payment system
- var payment_success = await process_payment_in_your_system()
-
- if payment_success:
- # Step 3: Create token and report to Google Play
- var token = await iap.create_alternative_billing_token_android()
-
- if token:
- # Report token to Google Play backend within 24 hours
- await report_token_to_google_play(token)`}
- ),
- }}
-
-
-
-
- Important: The token from Step 3 must be reported
- to Google Play backend within 24 hours. See{' '}
-
- Google's Alternative Billing documentation
- {' '}
- for backend integration details.
-
-
-
-
-
- Deprecated: The above APIs are deprecated in Google
- Play Billing Library 8.2.0+. For new implementations, use the{' '}
- Billing Programs API below.
-
-
-
-
-
-
- Billing Programs API (8.2.0+)
-
-
- Google Play Billing Library 8.2.0 introduces the new Billing Programs
- API which replaces the legacy alternative billing APIs. This provides
- better support for External Content Links and External Offers.
-
-
-
-
- Recommended: Use Billing Library 8.2.1+ as version
- 8.2.0 had bugs in isBillingProgramAvailableAsync and{' '}
- createBillingProgramReportingDetailsAsync.
-
-
-
-
- enableBillingProgramAndroid
-
-
- Step 0: Enable a billing program before calling{' '}
- initConnection(). Must be called during BillingClient
- setup.
-
- {`// Call BEFORE initConnection()
-// program: BillingProgramAndroid.ExternalOffer or BillingProgramAndroid.ExternalContentLink
-fun enableBillingProgram(program: BillingProgramAndroid)`}
-
-
- isBillingProgramAvailableAndroid
-
-
- Step 1: Check if a billing program is available for
- the current user.
-
- {`// Returns BillingProgramAvailabilityResultAndroid with isAvailable flag
-// Throws OpenIapError.NotPrepared if billing client not ready
-suspend fun isBillingProgramAvailable(
- program: BillingProgramAndroid
-): BillingProgramAvailabilityResultAndroid`}
-
-
- launchExternalLinkAndroid
-
-
- Step 2: Launch external link flow. Shows Play Store
- dialog and optionally launches external URL.
-
- Step 3: Create reporting details after successful
- payment. Returns external transaction token for reporting.
-
-
-
- Note: This API uses{' '}
- BillingProgramReportingDetailsParams internally, which
- requires Billing Library 8.3.0+. OpenIAP handles this automatically.
-
-
- {`// Returns BillingProgramReportingDetailsAndroid with externalTransactionToken
-// Token must be reported to Google Play backend within 24 hours
-// Throws OpenIapError.NotPrepared if billing client not ready
-suspend fun createBillingProgramReportingDetails(
- program: BillingProgramAndroid
-): BillingProgramReportingDetailsAndroid`}
-
-
-
-
- Billing Programs Flow Example
-
-
- {{
- typescript: (
- {`import {
- enableBillingProgramAndroid,
- isBillingProgramAvailableAndroid,
- launchExternalLinkAndroid,
- createBillingProgramReportingDetailsAndroid,
- initConnection,
-} from 'expo-iap';
-
-// Step 0: Enable billing program BEFORE initConnection
-enableBillingProgramAndroid('EXTERNAL_OFFER');
-
-await initConnection();
-
-async function handleExternalPurchase() {
- // Step 1: Check availability
- const result = await isBillingProgramAvailableAndroid('EXTERNAL_OFFER');
- if (!result.isAvailable) {
- return; // Not available for this user
- }
-
- // Step 2: Launch external link
- 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; // Failed to launch
- }
-
- // Process payment in your payment system
- const paymentSuccess = await processPaymentInYourSystem();
-
- if (paymentSuccess) {
- // Step 3: Create reporting details
- const details = await createBillingProgramReportingDetailsAndroid('EXTERNAL_OFFER');
-
- // Report token to Google Play backend within 24 hours
- await reportTokenToGooglePlay(details.externalTransactionToken);
- }
-}`}
- ),
- kotlin: (
- {`// Step 0: Enable billing program BEFORE initConnection
-openIapStore.enableBillingProgram(BillingProgramAndroid.ExternalOffer)
-
-openIapStore.initConnection(null)
-
-suspend fun handleExternalPurchase() {
- // Step 1: Check availability
- val result = openIapStore.isBillingProgramAvailable(
- BillingProgramAndroid.ExternalOffer
- )
- if (!result.isAvailable) {
- return // Not available for this user
- }
-
- // Step 2: Launch external link
- val launched = openIapStore.launchExternalLink(
- activity,
- LaunchExternalLinkParamsAndroid(
- billingProgram = BillingProgramAndroid.ExternalOffer,
- launchMode = ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp,
- linkType = ExternalLinkTypeAndroid.LinkToDigitalContentOffer,
- linkUri = "https://your-payment-site.com/checkout"
- )
- )
-
- if (!launched) {
- return // Failed to launch
- }
-
- // Process payment in your payment system
- val paymentSuccess = processPaymentInYourSystem()
-
- if (paymentSuccess) {
- // Step 3: Create reporting details
- val details = openIapStore.createBillingProgramReportingDetails(
- BillingProgramAndroid.ExternalOffer
- )
-
- // Report token to Google Play backend within 24 hours
- reportTokenToGooglePlay(details.externalTransactionToken)
- }
-}`}
- ),
- dart: (
- {`// Step 0: Enable billing program BEFORE initConnection
-FlutterInappPurchase.instance.enableBillingProgramAndroid(
- BillingProgramAndroid.externalOffer,
-);
-
-await FlutterInappPurchase.instance.initConnection();
-
-Future handleExternalPurchase() async {
- // Step 1: Check availability
- final result = await FlutterInappPurchase.instance
- .isBillingProgramAvailableAndroid(BillingProgramAndroid.externalOffer);
- if (!result.isAvailable) {
- return; // Not available for this user
- }
-
- // Step 2: Launch external link
- final launched = await FlutterInappPurchase.instance.launchExternalLinkAndroid(
- LaunchExternalLinkParamsAndroid(
- billingProgram: BillingProgramAndroid.externalOffer,
- launchMode: ExternalLinkLaunchModeAndroid.launchInExternalBrowserOrApp,
- linkType: ExternalLinkTypeAndroid.linkToDigitalContentOffer,
- linkUri: 'https://your-payment-site.com/checkout',
- ),
- );
-
- if (!launched) {
- return; // Failed to launch
- }
-
- // Process payment in your payment system
- final paymentSuccess = await processPaymentInYourSystem();
-
- if (paymentSuccess) {
- // Step 3: Create reporting details
- final details = await FlutterInappPurchase.instance
- .createBillingProgramReportingDetailsAndroid(
- BillingProgramAndroid.externalOffer,
- );
-
- // Report token to Google Play backend within 24 hours
- await reportTokenToGooglePlay(details.externalTransactionToken);
- }
-}`}
- ),
- gdscript: (
- {`# Step 0: Enable billing program BEFORE initConnection
-func _ready() -> void:
- iap.enable_billing_program_android(BillingProgramAndroid.EXTERNAL_OFFER)
- await iap.init_connection()
-
-func handle_external_purchase():
- # Step 1: Check availability
- var result = await iap.is_billing_program_available_android(
- BillingProgramAndroid.EXTERNAL_OFFER
- )
- if not result.is_available:
- return # Not available for this user
-
- # Step 2: Launch external link
- 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 # Failed to launch
-
- # Process payment in your payment system
- var payment_success = await process_payment_in_your_system()
-
- if payment_success:
- # Step 3: Create reporting details
- var details = await iap.create_billing_program_reporting_details_android(
- BillingProgramAndroid.EXTERNAL_OFFER
- )
-
- # Report token to Google Play backend within 24 hours
- await report_token_to_google_play(details.external_transaction_token)`}
- ),
- }}
-
-
-
-
-
- API Migration Guide
-
-
- Migrate from legacy Alternative Billing APIs to Billing Programs API:
-
-
-
-
-
Legacy API (6.2+)
-
New API (8.2.0+)
-
-
-
-
-
- checkAlternativeBillingAvailability()
-
-
- isBillingProgramAvailable(program)
-
-
-
-
- showAlternativeBillingInformationDialog()
-
-
- launchExternalLink(activity, params)
-
-
-
-
- createAlternativeBillingReportingToken()
-
-
- createBillingProgramReportingDetails(program)
-
-
-
-
- enableAlternativeBillingOnly()
-
-
- enableBillingProgram(program)
-
-
-
-
-
-
-
- See Also:{' '}
-
- External Purchase Guide
- {' '}
- for complete implementation details and examples.
-
-
-
-
- );
-}
-
-export default AndroidAPIs;
diff --git a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx
new file mode 100644
index 000000000..9c047fcd3
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx
@@ -0,0 +1,74 @@
+import { Link } from 'react-router-dom';
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function AcknowledgePurchaseAndroid() {
+ useScrollToHash();
+
+ return (
+
+
+
+ Android{' '}
+ acknowledgePurchaseAndroid
+
+
+ Acknowledge a non-consumable purchase or subscription. Required within 3
+ days or the purchase will be refunded.
+
+
+
+
+ ⚠️ Deprecated in Google Play Billing Library 8.2.0+.{' '}
+ Direct acknowledge / consume calls are being phased out — use the
+ cross-platform{' '}
+
+ finishTransaction
+ {' '}
+ API instead, which handles acknowledgment automatically and is the
+ recommended path for new code.
+
+ Step 1 of alternative billing flow. Check if alternative billing is
+ available for this user/device.
+
+
+
Signature
+
+ {{
+ kotlin: (
+ {`// Returns true if available, false otherwise
+// Throws OpenIapError.NotPrepared if billing client not ready
+suspend fun checkAlternativeBillingAvailability(): Boolean`}
+ ),
+ }}
+
+
+ );
+}
+
+export default CheckAlternativeBillingAvailabilityAndroid;
diff --git a/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx
new file mode 100644
index 000000000..4f3fa0a25
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx
@@ -0,0 +1,62 @@
+import { Link } from 'react-router-dom';
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function ConsumePurchaseAndroid() {
+ useScrollToHash();
+
+ return (
+
+
+
+ Android{' '}
+ consumePurchaseAndroid
+
+
+ Consume a consumable purchase, allowing repurchase. Automatically
+ acknowledges the purchase.
+
+
+
+
+ ⚠️ Deprecated in Google Play Billing Library 8.2.0+.{' '}
+ Use{' '}
+
+ finishTransaction
+ {' '}
+ with isConsumable: true instead — the unified path
+ consumes (or acknowledges) the purchase automatically and stays
+ forward-compatible with the new Billing Programs API.
+
+ Step 3 of alternative billing flow. Create external transaction token
+ for Google Play reporting.
+
+
+
Signature
+
+ {{
+ kotlin: (
+ {`// Token must be reported to Google Play backend within 24 hours
+// Returns token string, or null if creation failed
+suspend fun createAlternativeBillingToken(): String?`}
+ ),
+ }}
+
+
+ );
+}
+
+export default CreateAlternativeBillingTokenAndroid;
diff --git a/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx b/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx
new file mode 100644
index 000000000..7cf943d81
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx
@@ -0,0 +1,43 @@
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function CreateBillingProgramReportingDetailsAndroid() {
+ useScrollToHash();
+
+ return (
+
+ Step 3 of Billing Programs API. Create reporting details with external
+ transaction token after successful payment.
+
+
+
Signature
+
+ {{
+ kotlin: (
+ {`// Returns BillingProgramReportingDetailsAndroid with externalTransactionToken
+// Token must be reported to Google Play backend within 24 hours
+// Throws OpenIapError.NotPrepared if billing client not ready
+suspend fun createBillingProgramReportingDetails(
+ program: BillingProgramAndroid
+): BillingProgramReportingDetailsAndroid`}
+ ),
+ }}
+
+
+ );
+}
+
+export default CreateBillingProgramReportingDetailsAndroid;
diff --git a/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx b/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx
new file mode 100644
index 000000000..39452d6c3
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx
@@ -0,0 +1,87 @@
+import { Link } from 'react-router-dom';
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function EnableBillingProgramAndroid() {
+ useScrollToHash();
+
+ return (
+
+
+
+ Android{' '}
+ enableBillingProgramAndroid
+
+
+ Enables a billing program for Android (Billing Library 8.2.0+). Pass it
+ as the{' '}
+
+ enableBillingProgramAndroid
+ {' '}
+ field of{' '}
+
+ InitConnectionConfig
+ {' '}
+ when calling initConnection() — there is no separate
+ top-level call.
+
+
+
Signature
+
+ {{
+ typescript: (
+ {`// expo-iap
+import { initConnection } from 'expo-iap';
+// Same API in react-native-iap:
+// import { initConnection } from 'react-native-iap';
+
+await initConnection({
+ enableBillingProgramAndroid: 'external-offer',
+ // 'user-choice-billing' | 'external-content-link' | 'external-offer' | 'external-payments'
+});
+
+// --- Or via the useIAP() hook (also exported from react-native-iap) ---
+// useIAP auto-connects on mount and accepts the same enableBillingProgramAndroid
+// option directly, so the billing program is wired without an explicit
+// initConnection() call.
+import { useIAP } from 'expo-iap';
+
+function App() {
+ useIAP({ enableBillingProgramAndroid: 'external-offer' });
+
+ return ;
+}`}
+ ),
+ kotlin: (
+ {`openIapStore.initConnection(
+ InitConnectionConfig(
+ enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer
+ )
+)`}
+ ),
+ dart: (
+ {`await FlutterInappPurchase.instance.initConnection(
+ config: InitConnectionConfig(
+ enableBillingProgramAndroid: BillingProgramAndroid.externalOffer,
+ ),
+);`}
+ ),
+ gdscript: (
+ {`var config = InitConnectionConfig.new()
+config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER
+await iap.init_connection(config)`}
+ ),
+ }}
+
+
+ );
+}
+
+export default EnableBillingProgramAndroid;
diff --git a/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx b/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx
new file mode 100644
index 000000000..caee40b37
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx
@@ -0,0 +1,42 @@
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function IsBillingProgramAvailableAndroid() {
+ useScrollToHash();
+
+ return (
+
+ Step 2 of alternative billing flow. Show alternative billing information
+ dialog before processing payment.
+
+
+
Signature
+
+ {{
+ kotlin: (
+ {`// Returns true if user accepted, false if user canceled
+// Throws OpenIapError.NotPrepared if billing client not ready
+suspend fun showAlternativeBillingDialog(): Boolean`}
+ ),
+ }}
+
+
+ );
+}
+
+export default ShowAlternativeBillingDialogAndroid;
diff --git a/packages/docs/src/pages/docs/apis/connection.tsx b/packages/docs/src/pages/docs/apis/connection.tsx
deleted file mode 100644
index 17cc5e200..000000000
--- a/packages/docs/src/pages/docs/apis/connection.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-import { Link } from 'react-router-dom';
-import AnchorLink from '../../../components/AnchorLink';
-import CodeBlock from '../../../components/CodeBlock';
-import LanguageTabs from '../../../components/LanguageTabs';
-import SEO from '../../../components/SEO';
-import TLDRBox from '../../../components/TLDRBox';
-import { useScrollToHash } from '../../../hooks/useScrollToHash';
-
-function ConnectionAPIs() {
- useScrollToHash();
-
- return (
-
-
-
Connection APIs
-
- Manage the connection to the platform's billing service. These APIs must
- be called before any other IAP operations.
-
+
+ {{
+ typescript: (
+ {`// expo-iap
+import { endConnection } from 'expo-iap';
+// Same API in react-native-iap:
+// import { endConnection } from 'react-native-iap';
+
+// In React useEffect cleanup
+useEffect(() => {
+ void initConnection();
+
+ return () => {
+ void endConnection();
+ };
+}, []);
+
+// --- Or via the useIAP() hook (also exported from react-native-iap) ---
+// useIAP automatically calls endConnection() when the component unmounts,
+// so you only need the module-level call when you want to tear the
+// connection down outside of the hook's lifecycle (e.g. on sign-out).
+import { useIAP } from 'expo-iap';
+
+function PurchaseScreen() {
+ const { connected } = useIAP();
+
+ // No explicit endConnection() call needed — the hook handles cleanup.
+ return Store ready: {String(connected)};
+}`}
+ ),
+ swift: (
+ {`try await OpenIapModule.shared.endConnection()`}
+ ),
+ kotlin: (
+ {`openIapStore.endConnection()`}
+ ),
+ kmp: (
+ {`kmpIAP.endConnection()`}
+ ),
+ dart: (
+ {`await FlutterInappPurchase.instance.endConnection();`}
+ ),
+ gdscript: (
+ {`# In _exit_tree or cleanup
+func _exit_tree():
+ await iap.end_connection()`}
+ ),
+ }}
+
+
+ );
+}
+
+export default EndConnection;
diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx
new file mode 100644
index 000000000..4245acaf6
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx
@@ -0,0 +1,198 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function FetchProducts() {
+ useScrollToHash();
+
+ return (
+
+
+
fetchProducts
+
Retrieve products or subscriptions from the store by SKU.
+
+
+ Note about request* APIs
+
+
+
+ ℹ️{' '}
+ 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: APIs in this library that do{' '}
+ start with request (
+
+ requestPurchase
+
+ , requestPurchaseOnPromotedProductIOS) are{' '}
+ event-based. Their return values are 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
+
+ {{
+ typescript: (
+ {`fetchProducts(params: ProductRequest): Promise
+
+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;`}
+ ),
+ swift: (
+ {`func fetchProducts(_ params: ProductRequest) async throws -> FetchProductsResult`}
+ ),
+ kotlin: (
+ {`suspend fun fetchProducts(request: ProductRequest): FetchProductsResult`}
+ ),
+ kmp: (
+ {`suspend fun fetchProducts(request: ProductRequest): FetchProductsResult`}
+ ),
+ dart: (
+ {`Future fetchProducts({
+ required List skus,
+ ProductQueryType? type,
+});`}
+ ),
+ gdscript: (
+ {`# 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) -> Array`}
+ ),
+ }}
+
+
+
+
+ {{
+ typescript: (
+ {`// expo-iap
+import { finishTransaction, purchaseUpdatedListener } from 'expo-iap';
+// Same API in react-native-iap:
+// import { finishTransaction, purchaseUpdatedListener } from 'react-native-iap';
+
+purchaseUpdatedListener(async (purchase) => {
+ const verified = await verifyOnServer(purchase);
+ if (!verified) return;
+
+ await grantProduct(purchase.productId);
+
+ const isConsumable = purchase.productId.includes('coins');
+ await finishTransaction({ purchase, isConsumable });
+});
+
+// --- Or via the useIAP() hook (also exported from react-native-iap) ---
+// useIAP wires the purchase listener for you; finish the transaction inside
+// the onPurchaseSuccess callback.
+import { useIAP } from 'expo-iap';
+
+function PurchaseScreen() {
+ const { finishTransaction } = useIAP({
+ onPurchaseSuccess: async (purchase) => {
+ const verified = await verifyOnServer(purchase);
+ if (!verified) return;
+
+ await grantProduct(purchase.productId);
+ const isConsumable = purchase.productId.includes('coins');
+ await finishTransaction({ purchase, isConsumable });
+ },
+ });
+
+ return null;
+}`}
+ ),
+ swift: (
+ {`try await OpenIapModule.shared.finishTransaction(purchase, isConsumable: false)`}
+ ),
+ kotlin: (
+ {`openIapStore.finishTransaction(purchase, isConsumable = false)`}
+ ),
+ kmp: (
+ {`kmpIAP.finishTransaction(purchase, isConsumable = false)
+
+// --- Or via the DSL API ---
+// requestPurchase { } returns a Purchase you pass through
+// .toPurchaseInput() into finishTransaction. Use isConsumable = true for
+// consumables, false for subscriptions / non-consumables.
+val purchase = kmpIAP.requestPurchase {
+ ios { sku = "com.app.coins_100" }
+ android { skus = listOf("com.app.coins_100") }
+}
+
+// After server-side validation:
+kmpIAP.finishTransaction(
+ purchase = purchase.toPurchaseInput(),
+ isConsumable = true
+)`}
+ ),
+ dart: (
+ {`await FlutterInappPurchase.instance.finishTransaction(purchase);`}
+ ),
+ gdscript: (
+ {`await iap.finish_transaction(purchase, false)`}
+ ),
+ }}
+
+
+
+
+ Critical: Android purchases must be acknowledged
+ within 3 days or they will be automatically refunded. iOS transactions
+ will replay on every app launch if not finished.
+
+
+
+ );
+}
+
+export default FinishTransaction;
diff --git a/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx
new file mode 100644
index 000000000..b168f3bca
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx
@@ -0,0 +1,112 @@
+import { Link } from 'react-router-dom';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function GetActiveSubscriptions() {
+ useScrollToHash();
+
+ return (
+
+
+
getActiveSubscriptions
+
+ Get all active subscriptions with detailed renewal status information.
+
- Complete API reference for OpenIAP. APIs are organized by functionality
- to help you find what you need quickly.
+ Complete function reference for OpenIAP. Every public function is listed
+ below with a one-line description and a link to its full signature. For
+ higher-level guides see{' '}
+ Features.
-
-
-
-
- Connection
-
- : Initialize and manage store connection
-
Initialize the store connection. Call before any IAP API.
+
+
+
+
+ endConnection
+
+
+
Close the store connection and release resources.
+
+
+
+
+
+
+
+ Products
+
+
+
+
+
Function
+
Description
+
+
+
+
+
+
+ fetchProducts
+
+
+
Fetch products or subscriptions from the store.
+
+
+
+
+ getAvailablePurchases
+
+
+
List active purchases for the current user.
+
+
+
+
-
Core APIs
-
Essential APIs used in every IAP implementation.
-
-
-
-
-
-
+
+ Purchase
+
+
+
+
+
Function
+
Description
+
+
+
+
+
+
+ requestPurchase
+
+
+
Initiate a purchase or subscription flow.
+
+
+
+
+ finishTransaction
+
+
+
+ Complete a transaction after server-side verification. Required
+ on Android within 3 days.
+
+
+
+
+
+ restorePurchases
+
+
+
Restore non-consumable and active subscription purchases.
+
+
+
+
+ getStorefront
+
+
+
Return the user's storefront country code.
+
+
+
-
Advanced APIs
-
Additional APIs for validation and debugging.
-
-
-
-
+
+ Subscription
+
+
+
+
+
Function
+
Description
+
+
+
+
+
+
+ getActiveSubscriptions
+
+
+
Get details of all currently active subscriptions.
+
+
+
+
+ hasActiveSubscriptions
+
+
+
Check whether the user has any active subscription.
+
+
+
+
+ deepLinkToSubscriptions
+
+
+
Open the platform's subscription management UI.
+
+
+
-
Platform-Specific APIs
+
+ Validation
+
- APIs available only on specific platforms. Use these for
- platform-specific features.
+ Server-side verification helpers. Full walkthrough lives on{' '}
+ Features → Validation —
+ these signatures are listed here for completeness.
-
-
-
-
+
+
+
+
Function
+
Description
+
+
+
+
+
+
+ verifyPurchase
+
+
+
+ Verify a purchase against your own backend (returns{' '}
+ isValid + raw store metadata).
+
+
+
+
+
+ verifyPurchaseWithProvider
+
+
+
+ Verify via a managed provider (IAPKit, Apple, Google, Horizon)
+ without standing up your own server.
+
+ Check whether alternative billing is available for the user.
+
+
+
+
+
+ showAlternativeBillingDialogAndroid
+
+
+
Display Google's alternative billing information dialog.
+
+
+
+
+ createAlternativeBillingTokenAndroid
+
+
+
Create a reporting token for an alternative billing flow.
+
+
+
+
+ enableBillingProgramAndroid
+
+
+
+ Enable a Play Billing Program (Play Billing 8.2.0+). Note: this
+ is a config field of{' '}
+
+ InitConnectionConfig
+ {' '}
+ passed to initConnection(), not a standalone
+ mutation; the page documents the config-flow shape.
+
+
+
+
+
+ isBillingProgramAvailableAndroid
+
+
+
+ Check whether a billing program (e.g., External Payments) is
+ available for the current user.
+
+
+
+
+
+ launchExternalLinkAndroid
+
+
+
+ Launch an external content / offer link from inside the Billing
+ Programs flow (Play Billing 8.2.0+).
+
- Important: APIs starting with request{' '}
- are event-based operations, not promise-based.
-
-
- While these APIs return values for various purposes, you should{' '}
-
- not rely on their return values for actual purchase results
-
- . Instead, listen for events through{' '}
- purchaseUpdatedListener or{' '}
- purchaseErrorListener.
-
-
- This is because Apple's purchase system is fundamentally
- event-based, not promise-based. For more details, see this{' '}
-
- issue comment
-
- .
-
-
- The request prefix indicates that these are event
- requests - use the appropriate listeners to handle the actual
- results.
-
-
-
- See: Events for setting up purchase
- listeners.
-
-
Transaction vs Purchase
@@ -313,13 +763,18 @@ function APIsIndex() {
- OpenIAP normalizes this to Purchase in cross-platform
- APIs for consistency, while platform-specific APIs may use the native
- terminology.
+ OpenIAP normalizes this to{' '}
+
+ Purchase
+ {' '}
+ in cross-platform APIs for consistency, while platform-specific APIs
+ may use the native terminology.
diff --git a/packages/docs/src/pages/docs/apis/init-connection.tsx b/packages/docs/src/pages/docs/apis/init-connection.tsx
new file mode 100644
index 000000000..7c17a4156
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/init-connection.tsx
@@ -0,0 +1,145 @@
+import { Link } from 'react-router-dom';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function InitConnection() {
+ useScrollToHash();
+
+ return (
+
+
+
initConnection
+
+ Initialize connection to the store service. Must be called before any
+ other IAP operations.
+
- Get the full StoreKit 2 transaction history as PurchaseIOS values.
- Requires the SK2ConsumableTransactionHistory Info.plist key for
- finished consumables to be included (iOS 18+).
-
- In StoreKit 2, promoted products can be purchased directly via the
- standard purchase flow. When a user taps a promoted product in the App
- Store, the promotedProductListenerIOS event fires with
- the product ID. Use this ID to call requestPurchase(){' '}
- directly.
-
+ );
+}
+
+export default CurrentEntitlementIOS;
diff --git a/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx
new file mode 100644
index 000000000..b236cd207
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx
@@ -0,0 +1,39 @@
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function GetAllTransactionsIOS() {
+ useScrollToHash();
+
+ return (
+
+
+
+ iOS{' '}
+ getAllTransactionsIOS
+
+
+ Get the full StoreKit 2 transaction history as PurchaseIOS values.
+ Requires the SK2ConsumableTransactionHistory Info.plist key for finished
+ consumables to be included (iOS 18+).
+
+ Fetch an external-purchase token for the{' '}
+
+ ExternalPurchaseCustomLink
+ {' '}
+ API (iOS 18.1+). Pair the returned token with Apple's External Purchase
+ Server API to report acquisition or services transactions.
+
+ tokenType is{' '}
+ ExternalPurchaseCustomLinkTokenTypeIOS.acquisition for new
+ customers or{' '}
+ ExternalPurchaseCustomLinkTokenTypeIOS.services for
+ existing ones. The result wraps the opaque token plus expiration
+ metadata.
+
+
+ );
+}
+
+export default GetExternalPurchaseCustomLinkTokenIOS;
diff --git a/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx
new file mode 100644
index 000000000..25cc08985
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx
@@ -0,0 +1,35 @@
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function GetPendingTransactionsIOS() {
+ useScrollToHash();
+
+ return (
+
+
+
+ iOS{' '}
+ getPendingTransactionsIOS
+
+
Retrieve all pending transactions in the StoreKit queue.
+
+ {{
+ typescript: (
+ {`// expo-iap
+import { getReceiptDataIOS } from 'expo-iap';
+// Same API in react-native-iap:
+// import { getReceiptDataIOS } from 'react-native-iap';
+
+const receipt = await getReceiptDataIOS();
+// Send the base64-encoded receipt to your server for legacy verifyReceipt.
+console.log(receipt?.length ?? 0, 'bytes');
+
+// --- Or alongside the useIAP() hook (also exported from react-native-iap) ---
+// getReceiptDataIOS is a module-level helper; useIAP doesn't expose it on the
+// hook return, so call the module function from inside your component once
+// the hook reports the connection is ready.
+import { useIAP } from 'expo-iap';
+
+function ReceiptUploader() {
+ const { connected } = useIAP();
+
+ const upload = async () => {
+ if (!connected) return;
+ const receipt = await getReceiptDataIOS();
+ if (!receipt) return;
+ await fetch('/api/validate-receipt', {
+ method: 'POST',
+ body: JSON.stringify({ receipt }),
+ });
+ };
+
+ return ;
+}`}
+ ),
+ swift: (
+ {`let receipt = try await OpenIapModule.shared.getReceiptDataIOS()`}
+ ),
+ kotlin: (
+ {`val receipt = openIapStore.getReceiptDataIOS()`}
+ ),
+ kmp: (
+ {`val receipt = kmpIAP.getReceiptDataIOS()`}
+ ),
+ dart: (
+ {`final receipt = await FlutterInappPurchase.instance.getReceiptDataIOS();`}
+ ),
+ }}
+
+
+
+ iOS receipts contain all transactions for the bundle,
+ not just the latest one. For per-transaction validation prefer{' '}
+
+ getTransactionJwsIOS
+
+ . If the receipt file has not yet been written (e.g. immediately after
+ the very first purchase), Apple recommends calling{' '}
+
+ syncIOS
+ {' '}
+ and retrying.
+
+
+ );
+}
+
+export default GetReceiptDataIOS;
diff --git a/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx
new file mode 100644
index 000000000..1bd3f26e7
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx
@@ -0,0 +1,44 @@
+import { Link } from 'react-router-dom';
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function GetStorefrontIOS() {
+ useScrollToHash();
+
+ return (
+
+
+
+ iOS{' '}
+ getStorefrontIOS
+
+
Deprecated. Use getStorefront() (cross-platform) instead.
+
+
+
+ Deprecated. Use the cross-platform API. Use{' '}
+ getStorefront instead.
+
+
+ {{
+ typescript: (
+ {`// expo-iap
+import { getTransactionJwsIOS } from 'expo-iap';
+// Same API in react-native-iap:
+// import { getTransactionJwsIOS } from 'react-native-iap';
+
+const jws = await getTransactionJwsIOS('com.example.premium');
+if (jws) {
+ // Send the JWS to your server; verify it with Apple's public keys.
+ await fetch('/api/verify-transaction', {
+ method: 'POST',
+ body: JSON.stringify({ jws }),
+ });
+}
+
+// --- Or alongside the useIAP() hook (also exported from react-native-iap) ---
+// getTransactionJwsIOS is a module-level helper; useIAP doesn't expose it on
+// the hook return, so call the module function from inside your component.
+import { useIAP } from 'expo-iap';
+
+function ServerValidateButton({ sku }: { sku: string }) {
+ const { connected } = useIAP();
+
+ const validate = async () => {
+ if (!connected) return;
+ const jws = await getTransactionJwsIOS(sku);
+ if (!jws) return;
+ await api.verify(jws);
+ };
+
+ return ;
+}`}
+ ),
+ swift: (
+ {`let jws = try await OpenIapModule.shared.getTransactionJwsIOS(sku: "com.example.premium")`}
+ ),
+ kotlin: (
+ {`val jws = openIapStore.getTransactionJwsIOS(sku = "com.example.premium")`}
+ ),
+ kmp: (
+ {`val jws = kmpIAP.getTransactionJwsIOS(sku = "com.example.premium")`}
+ ),
+ dart: (
+ {`final jws = await FlutterInappPurchase.instance
+ .getTransactionJwsIOS('com.example.premium');`}
+ ),
+ }}
+
+
+
+ Returns the StoreKit 2 JWS representation of the most recent verified
+ transaction for the given product, or null when none
+ exists. Compare with{' '}
+
+ isTransactionVerifiedIOS
+ {' '}
+ for local-only checks, and{' '}
+
+ getReceiptDataIOS
+ {' '}
+ for the legacy bundle-wide receipt. See{' '}
+
+ Purchase
+ {' '}
+ for the parsed transaction shape.
+
+
+ );
+}
+
+export default GetTransactionJwsIOS;
diff --git a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx
new file mode 100644
index 000000000..3f6d6594f
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx
@@ -0,0 +1,47 @@
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function IsEligibleForExternalPurchaseCustomLinkIOS() {
+ useScrollToHash();
+
+ return (
+
+ Check whether the app is eligible to use the{' '}
+
+ ExternalPurchaseCustomLink
+ {' '}
+ API (iOS 18.1+). Returns true when the bundle is approved
+ for the corresponding entitlement and music-streaming-app-style flows
+ are allowed.
+
+
+ {{
+ typescript: (
+ {`// expo-iap
+import { isTransactionVerifiedIOS } from 'expo-iap';
+// Same API in react-native-iap:
+// import { isTransactionVerifiedIOS } from 'react-native-iap';
+
+const verified = await isTransactionVerifiedIOS('com.example.premium');
+if (!verified) {
+ // StoreKit 2 reported the JWS signature as unverified - don't grant entitlement.
+ return;
+}
+
+// --- Or alongside the useIAP() hook (also exported from react-native-iap) ---
+// isTransactionVerifiedIOS is a module-level helper; useIAP doesn't expose it
+// on the hook return, so call the module function from inside your component.
+import { useIAP } from 'expo-iap';
+
+function VerifyButton({ sku }: { sku: string }) {
+ const { connected } = useIAP();
+
+ const verify = async () => {
+ if (!connected) return;
+ const ok = await isTransactionVerifiedIOS(sku);
+ Alert.alert(ok ? 'Verified' : 'Verification failed');
+ };
+
+ return ;
+}`}
+ ),
+ swift: (
+ {`let isVerified = try await OpenIapModule.shared.isTransactionVerifiedIOS(sku: "com.example.premium")`}
+ ),
+ kotlin: (
+ {`val isVerified = openIapStore.isTransactionVerifiedIOS(sku = "com.example.premium")`}
+ ),
+ kmp: (
+ {`val isVerified = kmpIAP.isTransactionVerifiedIOS(sku = "com.example.premium")`}
+ ),
+ dart: (
+ {`final isVerified = await FlutterInappPurchase.instance
+ .isTransactionVerifiedIOS('com.example.premium');`}
+ ),
+ }}
+
+
+
+ Returns true when StoreKit 2 has locally verified the
+ transaction's JWS signature. For server-side validation, fetch the
+ signed payload with{' '}
+
+ getTransactionJwsIOS
+ {' '}
+ and verify it on your backend using Apple's public keys.
+
+
+ );
+}
+
+export default IsTransactionVerifiedIOS;
diff --git a/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx
new file mode 100644
index 000000000..1e96ec053
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx
@@ -0,0 +1,35 @@
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function LatestTransactionIOS() {
+ useScrollToHash();
+
+ return (
+
+
+
+ iOS{' '}
+ latestTransactionIOS
+
+
Get the most recent transaction for a product (iOS 15+).
+ Deprecated. Use promotedProductListenerIOS plus requestPurchase instead.
+
+
+
+
+ Deprecated. In StoreKit 2, promoted products fire
+ promotedProductListenerIOS with the productId — call requestPurchase
+ with that SKU. Use{' '}
+ requestPurchase instead.
+
+ Display the system disclosure notice for{' '}
+
+ ExternalPurchaseCustomLink
+ {' '}
+ (iOS 18.1+). Apple requires this sheet to be presented after a
+ deliberate customer interaction, before you can route the user to an
+ external purchase URL.
+
+ noticeType picks the disclosure style required by the flow
+ you are entering (e.g. .acquisition for first-time
+ payments, .services for ongoing services).
+
+
+ );
+}
+
+export default ShowExternalPurchaseCustomLinkNoticeIOS;
diff --git a/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx
new file mode 100644
index 000000000..eda7db092
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx
@@ -0,0 +1,39 @@
+import CodeBlock from '../../../../components/CodeBlock';
+import LanguageTabs from '../../../../components/LanguageTabs';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function ShowManageSubscriptionsIOS() {
+ useScrollToHash();
+
+ return (
+
+
+
+ iOS{' '}
+ showManageSubscriptionsIOS
+
+
+ Show in-app subscription management UI and detect status changes (iOS
+ 15+). Returns purchases for subscriptions whose auto-renewal status
+ changed.
+
- Android limitation: For subscriptions with multiple
- base plans, the currentPlanId field may be inaccurate.
- See{' '}
-
- basePlanId limitation
-
- .
-
-
-
-
- See: Purchase
-
-
-
- );
-}
-
-export default ProductsAPIs;
diff --git a/packages/docs/src/pages/docs/apis/purchase.tsx b/packages/docs/src/pages/docs/apis/purchase.tsx
deleted file mode 100644
index 9c484d51e..000000000
--- a/packages/docs/src/pages/docs/apis/purchase.tsx
+++ /dev/null
@@ -1,490 +0,0 @@
-import { Link } from 'react-router-dom';
-import AnchorLink from '../../../components/AnchorLink';
-import CodeBlock from '../../../components/CodeBlock';
-import LanguageTabs from '../../../components/LanguageTabs';
-import SEO from '../../../components/SEO';
-import TLDRBox from '../../../components/TLDRBox';
-import { useScrollToHash } from '../../../hooks/useScrollToHash';
-
-function PurchaseAPIs() {
- useScrollToHash();
-
- return (
-
-
-
Purchase APIs
-
- APIs for requesting purchases, completing transactions, and restoring
- previous purchases.
-
- ⚠️ Important: APIs starting with{' '}
- request are event-based operations, not promise-based.
-
-
- While these APIs return values for various purposes, you should{' '}
-
- not rely on their return values for actual purchase results
-
- . Instead, listen for events through{' '}
- purchaseUpdatedListener or{' '}
- purchaseErrorListener.
-
-
- This is because Apple's purchase system is fundamentally
- event-based, not promise-based. For more details, see this{' '}
-
- issue comment
-
- .
-
-
- The request prefix indicates that these are event
- requests - use the appropriate listeners to handle the actual
- results.
-
-
-
-
-
-
- requestPurchase
-
-
- Initiate a purchase flow. The result is delivered through{' '}
- purchaseUpdatedListener, not the return value.
-
-
- {{
- typescript: (
- {`import { finishTransaction, purchaseUpdatedListener } from 'expo-iap';
-
-purchaseUpdatedListener(async (purchase) => {
- // 1. Verify on your server
- const verified = await verifyOnServer(purchase);
- if (!verified) return;
-
- // 2. Grant entitlement to user
- await grantProduct(purchase.productId);
-
- // 3. Finish the transaction
- const isConsumable = purchase.productId.includes('coins');
- await finishTransaction(purchase, isConsumable);
-});`}
- ),
- swift: (
- {`try await OpenIapModule.shared.finishTransaction(purchase, isConsumable: false)`}
- ),
- kotlin: (
- {`openIapStore.finishTransaction(purchase, isConsumable = false)`}
- ),
- kmp: (
- {`kmpIAP.finishTransaction(purchase, isConsumable = false)`}
- ),
- dart: (
- {`await FlutterInappPurchase.instance.finishTransaction(purchase);`}
- ),
- gdscript: (
- {`# Handle purchase update
-func _on_purchase_updated(purchase: Purchase):
- # 1. Verify on your server
- var verified = await verify_on_server(purchase)
- if not verified:
- return
-
- # 2. Grant entitlement to user
- await grant_product(purchase.product_id)
-
- # 3. Finish the transaction
- var is_consumable = "coins" in purchase.product_id
- await iap.finish_transaction(purchase, is_consumable)`}
- ),
- }}
-
-
-
-
- Critical: Android purchases must be acknowledged
- within 3 days or they will be automatically refunded. iOS
- transactions will replay on every app launch if not finished.
-
-
-
-
-
-
- restorePurchases
-
-
- Restore completed transactions. Use this to implement a "Restore
- Purchases" button for users who reinstall the app.
-
- Returns the ISO 3166-1 alpha-2 country code. Returns an empty string
- when the storefront cannot be determined.
-
-
-
- );
-}
-
-export default PurchaseAPIs;
diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx
new file mode 100644
index 000000000..ade42a18d
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx
@@ -0,0 +1,267 @@
+import { Link } from 'react-router-dom';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function RequestPurchase() {
+ useScrollToHash();
+
+ return (
+
+
+
requestPurchase
+
+ Initiate a purchase flow. The result is delivered through
+ purchaseUpdatedListener, not the return value.
+
+
+
+
+ ⚠️ Important: APIs starting with request{' '}
+ are event-based operations, not promise-based.
+
+
+ While these APIs return values for various purposes, you should{' '}
+
+ not rely on their return values for actual purchase results
+
+ . Instead, listen for events through{' '}
+
+ purchaseUpdatedListener
+ {' '}
+ or{' '}
+
+ purchaseErrorListener
+
+ .
+
+
+ This is because Apple's purchase system is fundamentally event-based,
+ not promise-based. For more details, see{' '}
+
+ this issue comment
+
+ .
+
+
+ The request prefix indicates that these are event
+ requests — use the appropriate listeners to handle the actual results.
+
- iOS: Opens the Settings app subscription
- management. Also see{' '}
-
- showManageSubscriptionsIOS
- {' '}
- for an in-app UI.
-
-
- Android: Opens Google Play subscription management
- for the specified SKU.
-
-
-
-
- );
-}
-
-export default SubscriptionAPIs;
diff --git a/packages/docs/src/pages/docs/apis/validate-receipt.tsx b/packages/docs/src/pages/docs/apis/validate-receipt.tsx
new file mode 100644
index 000000000..0bb789766
--- /dev/null
+++ b/packages/docs/src/pages/docs/apis/validate-receipt.tsx
@@ -0,0 +1,14 @@
+import { Navigate } from 'react-router-dom';
+
+// Cross-platform `validateReceipt` is deprecated in the schema in favour
+// of `verifyPurchase`. Bookmarks that hit /docs/apis/validate-receipt
+// bounce to the canonical Validation feature page, so old links keep
+// working without us maintaining a parallel reference. Use
+// so the redirect happens declaratively during
+// render — no flash of intermediate content, no extra effect-driven
+// re-render.
+function ValidateReceipt() {
+ return ;
+}
+
+export default ValidateReceipt;
diff --git a/packages/docs/src/pages/docs/errors.tsx b/packages/docs/src/pages/docs/errors.tsx
index 0ec395322..540b83578 100644
--- a/packages/docs/src/pages/docs/errors.tsx
+++ b/packages/docs/src/pages/docs/errors.tsx
@@ -19,11 +19,10 @@ function Errors() {
Error Codes
-
Error Structure
+
Error Structure
All purchase errors follow a consistent structure for easy handling.
- See PurchaseError type{' '}
- for details.
+ The PurchaseError shape is defined below.
+ Complete listener reference for OpenIAP. Every event listener is listed
+ below with a one-line description and a link to its full signature. The
+ IAP library uses an event-driven architecture to handle purchase flows
+ asynchronously — set up listeners before initiating any purchase to
+ properly handle the results.
+
-
Event System Overview
+
+ Event System Overview
+
The IAP library uses an event-driven architecture to handle purchase
flows asynchronously. You must set up event listeners before
@@ -33,6 +42,7 @@ function Events() {
{`enum IapEvent {
PurchaseUpdated = 'purchaseUpdated',
PurchaseError = 'purchaseError',
+ SubscriptionBillingIssue = 'subscriptionBillingIssue',
PromotedProductIOS = 'promotedProductIOS',
UserChoiceBillingAndroid = 'userChoiceBillingAndroid',
DeveloperProvidedBillingAndroid = 'developerProvidedBillingAndroid', // 8.3.0+
@@ -42,6 +52,7 @@ function Events() {
{`enum IapEvent {
case purchaseUpdated
case purchaseError
+ case subscriptionBillingIssue
case promotedProductIOS
}`}
),
@@ -49,6 +60,7 @@ function Events() {
{`enum class IapEvent {
PurchaseUpdated,
PurchaseError,
+ SubscriptionBillingIssue,
UserChoiceBillingAndroid,
DeveloperProvidedBillingAndroid // 8.3.0+
}`}
@@ -57,6 +69,7 @@ function Events() {
{`enum class IapEvent {
PurchaseUpdated,
PurchaseError,
+ SubscriptionBillingIssue,
UserChoiceBillingAndroid,
DeveloperProvidedBillingAndroid // 8.3.0+
}`}
@@ -65,6 +78,7 @@ function Events() {
{`enum IapEvent {
purchaseUpdated,
purchaseError,
+ subscriptionBillingIssue,
promotedProductIOS,
userChoiceBillingAndroid,
developerProvidedBillingAndroid, // 8.3.0+
@@ -74,1182 +88,125 @@ function Events() {
{`enum IapEvent {
PURCHASE_UPDATED = 0,
PURCHASE_ERROR = 1,
- PROMOTED_PRODUCT_IOS = 2,
- USER_CHOICE_BILLING_ANDROID = 3,
- DEVELOPER_PROVIDED_BILLING_ANDROID = 4, # 8.3.0+
-}`}
- ),
- }}
-
-
-
-
-
- Purchase Updated Event
-
-
- Fired when a purchase is successful or when a pending purchase is
- completed.
-
- The error event delivers a{' '}
- PurchaseError object with error
- details. See Error Codes for complete
- reference.
-
-
-
Error Handling Strategy
-
- Handle errors based on their{' '}
- error codes:
-
-
-
- UserCancelled - No action required
-
-
- ItemUnavailable - Check product availability
-
-
- NetworkError - Retry with backoff
-
-
- AlreadyOwned - Restore purchases
-
-
- ReceiptFailed - Retry validation
-
-
-
-
-
-
- Subscription Billing Issue Event
-
-
- Fired when an active subscription enters a state that needs user
- attention because of a payment problem — card declined, expired
- payment method, billing retry, or grace period. Unifies StoreKit 2{' '}
- Message.billingIssue (iOS 18+) and Play Billing{' '}
- Purchase.isSuspended (Play Billing Library 8.1+) under a
- single cross-platform stream. Silent no-op on platforms that cannot
- emit (tvOS, watchOS, visionOS, macOS, Meta Horizon).
-
- The emitted Purchase is a regular subscription payload —
- use productId, purchaseToken, and platform
- fields to prompt the user to update payment. Play deduplicates by{' '}
- purchaseToken per session; iOS fires per Message
- delivery.
-
-
-
- See{' '}
-
- Subscription Billing Issue feature guide
- {' '}
- for platform coverage, signal sources, and UX recommendations.
-
-
- Promoted Product Event (iOS)
+
+ Listeners
-
- Fired when a user clicks on a promoted in-app purchase in the App
- Store.
-
-
-
Listener Setup
-
- {{
- typescript: (
- {`promotedProductListenerIOS(
- listener: (productId: string) => void
-): Subscription`}
- ),
- swift: (
- {`// AsyncSequence approach
-var promotedProducts: AsyncStream
-
-// Combine approach
-var promotedProductPublisher: AnyPublisher`}
- ),
- kotlin: (
- {`// iOS only - not available on Android`}
- ),
- kmp: (
- {`// iOS only - not available on Android`}
- ),
- dart: (
- {`Stream get promotedProductStream; // iOS only`}
- ),
- }}
-
-
Registers a listener for App Store promoted product events.
-
-
- {{
- typescript: (
- {`import {
- promotedProductListenerIOS,
- fetchProducts,
- requestPurchase
-} from 'expo-iap';
-
-const subscription = promotedProductListenerIOS(async (productId) => {
- console.log('Promoted product tapped:', productId);
-
- // Fetch product details
- const products = await fetchProducts({
- skus: [productId],
- type: 'in-app'
- });
-
- if (products.length > 0) {
- // Show product info to user and confirm purchase
- const confirmed = await showPurchaseConfirmation(products[0]);
-
- if (confirmed) {
- // Purchase directly using requestPurchase with the received SKU
- await requestPurchase({
- request: { apple: { sku: productId } },
- type: 'in-app'
- });
- }
- }
-});
-
-// Cleanup when done
-subscription.remove();`}
- ),
- swift: (
- {`import OpenIap
-
-// Using async/await
-Task {
- for await productId in OpenIapModule.shared.promotedProducts {
- print("Promoted product tapped: \\(productId)")
-
- // Fetch product details
- let products = try await OpenIapModule.shared.fetchProducts(
- ProductRequest(skus: [productId], type: .inApp)
- )
-
- if let product = products.first {
- // Show product info to user and confirm purchase
- if await showPurchaseConfirmation(product) {
- // Purchase directly using requestPurchase with the received SKU
- try await OpenIapModule.shared.requestPurchase(
- RequestPurchaseProps(
- request: .purchase(RequestPurchasePropsByPlatforms(
- apple: RequestPurchaseIosProps(sku: productId)
- )),
- type: .inApp
- )
- )
- }
- }
- }
-}
-
-// Or using Combine
-OpenIapModule.shared.promotedProductPublisher
- .sink { productId in
- print("Promoted product: \\(productId)")
- }
- .store(in: &cancellables)`}
- ),
- dart: (
- {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
-
-// iOS only - will not fire on Android
-final subscription = FlutterInappPurchase.promotedProductIOS.listen((productId) async {
- print('Promoted product tapped: $productId');
-
- // Fetch product details
- final products = await FlutterInappPurchase.instance.fetchProducts(
- ProductRequest(skus: [productId!], type: ProductQueryType.inApp),
- );
-
- if (products.isNotEmpty) {
- // Show product info to user and confirm purchase
- final confirmed = await showPurchaseConfirmation(products.first);
-
- if (confirmed) {
- // Purchase directly using requestPurchase with the received SKU
- await FlutterInappPurchase.instance.requestPurchase(
- RequestPurchaseProps(
- request: RequestPurchasePropsByPlatforms(
- apple: RequestPurchaseIosProps(sku: productId!),
- ),
- type: ProductQueryType.inApp,
- ),
- );
- }
- }
-});
-
-// Cleanup when done
-subscription.cancel();`}
- ),
- }}
-
-
-
- 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 can be
- purchased directly via the standard requestPurchase(){' '}
- flow. The deprecated{' '}
-
- requestPurchaseOnPromotedProductIOS()
- {' '}
- API is no longer needed.
-
-
-
-
-
-
- User Choice Billing Event (Android)
-
-
- Fired when a user selects alternative billing in the User Choice
- Billing dialog on Android.
-
- Registers a listener for User Choice Billing events. This listener is
- only triggered when the user selects alternative billing instead of
- Google Play billing.
-
-
-
- {{
- typescript: (
- {`import { userChoiceBillingListenerAndroid } from 'expo-iap';
-
-const subscription = userChoiceBillingListenerAndroid(async (details) => {
- console.log('User chose alternative billing');
- console.log('Products:', details.products);
- console.log('Token:', details.externalTransactionToken);
-
- // Process payment with your backend
- const paymentResult = await processPaymentWithBackend({
- products: details.products,
- token: details.externalTransactionToken,
- });
-
- if (paymentResult.success) {
- // Backend should report token to Google Play within 24 hours
- grantUserAccess(details.products);
- }
-});
-
-// Cleanup when done
-subscription.remove();`}
- ),
- kotlin: (
- {`import dev.hyo.openiap.UserChoiceBillingDetails
-
-// Using Flow
-lifecycleScope.launch {
- openIapStore.userChoiceBillingEvents.collect { details ->
- println("User chose alternative billing")
- println("Products: \${details.products}")
- println("Token: \${details.externalTransactionToken}")
-
- // Process payment with your backend
- val paymentResult = processPaymentWithBackend(
- products = details.products,
- token = details.externalTransactionToken
- )
-
- if (paymentResult.success) {
- // Backend should report token to Google Play within 24 hours
- grantUserAccess(details.products)
- }
- }
-}
-
-// Or with callback
-openIapStore.setUserChoiceBillingListener { details ->
- println("User chose alternative billing for: \${details.products}")
-}`}
- ),
- kmp: (
- {`import io.github.hyochan.kmpiap.KmpIAP
-
-val kmpIAP = KmpIAP()
-
-// Using Flow
-lifecycleScope.launch {
- kmpIAP.userChoiceBillingEvents.collect { details ->
- println("User chose alternative billing")
- println("Products: \${details.products}")
- println("Token: \${details.externalTransactionToken}")
-
- // Process payment with your backend
- val paymentResult = processPaymentWithBackend(
- products = details.products,
- token = details.externalTransactionToken
- )
-
- if (paymentResult.success) {
- // Backend should report token to Google Play within 24 hours
- grantUserAccess(details.products)
- }
- }
-}
-
-// Or with callback
-kmpIAP.setUserChoiceBillingListener { details ->
- println("User chose alternative billing for: \${details.products}")
-}`}
- ),
- dart: (
- {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
-
-// Android only - will not fire on iOS
-final subscription = FlutterInappPurchase.userChoiceBillingAndroid.listen((details) async {
- print('User chose alternative billing');
- print('Products: \${details?.products}');
- print('Token: \${details?.externalTransactionToken}');
-
- // Process payment with your backend
- final paymentResult = await processPaymentWithBackend(
- products: details!.products,
- token: details.externalTransactionToken,
- );
-
- if (paymentResult.success) {
- // Backend should report token to Google Play within 24 hours
- grantUserAccess(details.products);
- }
-});
-
-// Cleanup when done
-subscription.cancel();`}
- ),
- }}
-
-
-
Event Payload
-
- {{
- typescript: (
- {`interface UserChoiceBillingDetails {
- externalTransactionToken: string;
- products: string[];
-}`}
- ),
- swift: (
- {`// Android only - not available on iOS`}
- ),
- kotlin: (
- {`data class UserChoiceBillingDetails(
- val externalTransactionToken: String,
- val products: List
-)`}
- ),
- kmp: (
- {`data class UserChoiceBillingDetails(
- val externalTransactionToken: String,
- val products: List
-)`}
- ),
- dart: (
- {`class UserChoiceBillingDetails {
- final String externalTransactionToken;
- final List products;
-}`}
- ),
- }}
-
-
- externalTransactionToken - Token that must be
- reported to Google Play within 24 hours
-
- products - List of product IDs selected by the user
-
-
-
Handling User Choice Billing
-
-
- Receive UserChoiceBillingDetails via listener
-
-
Process payment with your backend payment system
-
Send the external transaction token to your backend
-
- Backend reports token to Google Play within 24 hours (required for
- compliance)
-
-
Grant user access to purchased content
-
-
-
-
- ⚠️ Important: The external transaction token MUST
- be reported to Google Play within 24 hours. Failure to report tokens
- may result in account suspension. It is strongly recommended to
- handle token reporting on your backend server for reliability and
- security.
-
-
-
-
Flow Comparison
-
- When using User Choice Billing mode, there are two possible flows
- depending on user selection:
-
-
-
- Google Play selected - Standard{' '}
- PurchaseUpdated event fires (handle normally)
-
-
- Alternative billing selected -{' '}
- UserChoiceBillingAndroid event fires (handle with your
- payment system)
-
- Fired when a user selects developer-provided billing in the External
- Payments flow on Android. This is different from User Choice Billing -
- it presents a side-by-side choice dialog in the purchase flow itself.
-
- Registers a listener for Developer Provided Billing events. This
- listener is only triggered when the user selects the developer's
- payment option (instead of Google Play) in the External Payments flow.
-
-
-
- {{
- typescript: (
- {`import { developerProvidedBillingListener } from 'expo-iap';
-
-const subscription = developerProvidedBillingListener(async (details) => {
- console.log('User selected developer billing');
- console.log('Token:', details.externalTransactionToken);
-
- // Process payment with your payment system
- const paymentResult = await processPaymentWithYourGateway({
- token: details.externalTransactionToken,
- // Your payment details
- });
-
- if (paymentResult.success) {
- // IMPORTANT: Report the token to Google Play within 24 hours
- await reportExternalTransactionToGoogle(details.externalTransactionToken);
- grantUserAccess();
- }
-});
-
-// Cleanup when done
-subscription.remove();`}
- ),
- kotlin: (
- {`import dev.hyo.openiap.DeveloperProvidedBillingDetailsAndroid
-
-// Using callback
-openIapStore.addDeveloperProvidedBillingListener { details ->
- println("User selected developer billing")
- println("Token: \${details.externalTransactionToken}")
-
- lifecycleScope.launch {
- // Process payment with your payment system
- val paymentResult = processPaymentWithYourGateway(
- token = details.externalTransactionToken
- )
-
- if (paymentResult.success) {
- // IMPORTANT: Report the token to Google Play within 24 hours
- reportExternalTransactionToGoogle(details.externalTransactionToken)
- grantUserAccess()
- }
- }
-}`}
- ),
- kmp: (
- {`import io.github.hyochan.kmpiap.KmpIAP
-
-val kmpIAP = KmpIAP()
-
-// Using callback
-kmpIAP.addDeveloperProvidedBillingListener { details ->
- println("User selected developer billing")
- println("Token: \${details.externalTransactionToken}")
-
- lifecycleScope.launch {
- // Process payment with your payment system
- val paymentResult = processPaymentWithYourGateway(
- token = details.externalTransactionToken
- )
-
- if (paymentResult.success) {
- // IMPORTANT: Report the token to Google Play within 24 hours
- reportExternalTransactionToGoogle(details.externalTransactionToken)
- grantUserAccess()
- }
- }
-}`}
- ),
- dart: (
- {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
-
-// Android only (8.3.0+) - will not fire on iOS or older Android
-final subscription = FlutterInappPurchase.developerProvidedBillingStream
- .listen((details) async {
- print('User selected developer billing');
- print('Token: \${details.externalTransactionToken}');
-
- // Process payment with your payment system
- final paymentResult = await processPaymentWithYourGateway(
- token: details.externalTransactionToken,
- );
-
- if (paymentResult.success) {
- // IMPORTANT: Report the token to Google Play within 24 hours
- await reportExternalTransactionToGoogle(details.externalTransactionToken);
- grantUserAccess();
- }
-});
-
-// Cleanup when done
-subscription.cancel();`}
- ),
- }}
-
-
-
Event Payload
-
- {{
- typescript: (
- {`interface DeveloperProvidedBillingDetails {
- externalTransactionToken: string;
-}`}
- ),
- swift: (
- {`// Android only - not available on iOS`}
- ),
- kotlin: (
- {`data class DeveloperProvidedBillingDetailsAndroid(
- val externalTransactionToken: String
-)`}
- ),
- kmp: (
- {`data class DeveloperProvidedBillingDetailsAndroid(
- val externalTransactionToken: String
-)`}
- ),
- dart: (
- {`class DeveloperProvidedBillingDetails {
- final String externalTransactionToken;
-}`}
- ),
- }}
-
-
- externalTransactionToken - Token that must be
- reported to Google Play within 24 hours after completing the payment
-
-
-
Comparison: User Choice vs Developer Provided Billing
-
Feature
-
User Choice Billing
-
Developer Provided Billing
+
Listener
+
Description
-
Billing Library
-
7.0+
-
8.3.0+
+
+
+ purchaseUpdatedListener
+
+
+
+ Fires when a purchase is successful or a pending purchase is
+ completed.
+
+
+
+
+
+ purchaseErrorListener
+
+
+
Fires when a purchase fails or is cancelled by the user.
+
+
+
+
+ subscriptionBillingIssueListener
+
+
+
+ Fires when an active subscription enters a billing issue state
+ (iOS 18+ / Play Billing 8.1+; not emitted on Horizon).
+
+
+
+
+
+
+
+ iOS Listeners
+
+
+
-
Availability
-
Eligible regions
-
Japan only
+
Listener
+
Description
+
+
-
When presented
-
After initConnection()
-
During requestPurchase()
+
+
+ promotedProductListenerIOS
+
+
+
+ Fires when a user clicks on a promoted in-app purchase in the
+ App Store.
+
- enableBillingProgram(EXTERNAL_PAYMENTS) +{' '}
- developerBillingOption in requestPurchase
+ Fires when a user selects developer-provided billing in the
+ External Payments flow (8.3.0+, Japan only).
-
-
-
- ⚠️ Important: The external transaction token MUST
- be reported to Google Play within 24 hours using the{' '}
- externaltransactions.createexternaltransaction API.
- Failure to report tokens may result in account suspension.
-
+ Fired when a user selects developer-provided billing in the External
+ Payments flow on Android. This is different from User Choice Billing -
+ it presents a side-by-side choice dialog in the purchase flow itself.
+
+ Registers a listener for Developer Provided Billing events. This
+ listener is only triggered when the user selects the developer's payment
+ option (instead of Google Play) in the External Payments flow.
+
+
+
+ {{
+ typescript: (
+ {`import { developerProvidedBillingListenerAndroid } from 'expo-iap';
+
+const subscription = developerProvidedBillingListenerAndroid(async (details) => {
+ console.log('User selected developer billing');
+ console.log('Token:', details.externalTransactionToken);
+
+ // Process payment with your payment system
+ const paymentResult = await processPaymentWithYourGateway({
+ token: details.externalTransactionToken,
+ // Your payment details
+ });
+
+ if (paymentResult.success) {
+ // IMPORTANT: Report the token to Google Play within 24 hours
+ await reportExternalTransactionToGoogle(details.externalTransactionToken);
+ grantUserAccess();
+ }
+});
+
+// Cleanup when done
+subscription.remove();`}
+ ),
+ kotlin: (
+ {`import dev.hyo.openiap.DeveloperProvidedBillingDetailsAndroid
+
+// Using callback
+openIapStore.addDeveloperProvidedBillingListener { details ->
+ println("User selected developer billing")
+ println("Token: \${details.externalTransactionToken}")
+
+ lifecycleScope.launch {
+ // Process payment with your payment system
+ val paymentResult = processPaymentWithYourGateway(
+ token = details.externalTransactionToken
+ )
+
+ if (paymentResult.success) {
+ // IMPORTANT: Report the token to Google Play within 24 hours
+ reportExternalTransactionToGoogle(details.externalTransactionToken)
+ grantUserAccess()
+ }
+ }
+}`}
+ ),
+ kmp: (
+ {`import io.github.hyochan.kmpiap.KmpIAP
+
+val kmpIAP = KmpIAP()
+
+// Using callback
+kmpIAP.addDeveloperProvidedBillingListener { details ->
+ println("User selected developer billing")
+ println("Token: \${details.externalTransactionToken}")
+
+ lifecycleScope.launch {
+ // Process payment with your payment system
+ val paymentResult = processPaymentWithYourGateway(
+ token = details.externalTransactionToken
+ )
+
+ if (paymentResult.success) {
+ // IMPORTANT: Report the token to Google Play within 24 hours
+ reportExternalTransactionToGoogle(details.externalTransactionToken)
+ grantUserAccess()
+ }
+ }
+}`}
+ ),
+ dart: (
+ {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
+
+// Android only (8.3.0+) - will not fire on iOS or older Android
+final subscription = FlutterInappPurchase.developerProvidedBillingStream
+ .listen((details) async {
+ print('User selected developer billing');
+ print('Token: \${details.externalTransactionToken}');
+
+ // Process payment with your payment system
+ final paymentResult = await processPaymentWithYourGateway(
+ token: details.externalTransactionToken,
+ );
+
+ if (paymentResult.success) {
+ // IMPORTANT: Report the token to Google Play within 24 hours
+ await reportExternalTransactionToGoogle(details.externalTransactionToken);
+ grantUserAccess();
+ }
+});
+
+// Cleanup when done
+subscription.cancel();`}
+ ),
+ }}
+
+
+
Event Payload
+
+ {{
+ typescript: (
+ {`interface DeveloperProvidedBillingDetailsAndroid {
+ externalTransactionToken: string;
+}`}
+ ),
+ swift: (
+ {`// Android only - not available on iOS`}
+ ),
+ kotlin: (
+ {`data class DeveloperProvidedBillingDetailsAndroid(
+ val externalTransactionToken: String
+)`}
+ ),
+ kmp: (
+ {`data class DeveloperProvidedBillingDetailsAndroid(
+ val externalTransactionToken: String
+)`}
+ ),
+ dart: (
+ {`class DeveloperProvidedBillingDetailsAndroid {
+ final String externalTransactionToken;
+}`}
+ ),
+ }}
+
+
+ externalTransactionToken - Token that must be reported
+ to Google Play within 24 hours after completing the payment
+
+
+
Comparison: User Choice vs Developer Provided Billing
+
+
+
+
Feature
+
User Choice Billing
+
Developer Provided Billing
+
+
+
+
+
Billing Library
+
7.0+
+
8.3.0+
+
+
+
Availability
+
Eligible regions
+
Japan only
+
+
+
When presented
+
After initConnection()
+
During requestPurchase()
+
+
+
UI
+
Separate dialog before purchase
+
Side-by-side choice in purchase dialog
+
+
+
Event
+
+ UserChoiceBillingAndroid
+
+
+ DeveloperProvidedBillingAndroid
+
+
+
+
Setup
+
+ AlternativeBillingModeAndroid.UserChoice
+
+
+ enableBillingProgram(EXTERNAL_PAYMENTS) +{' '}
+ developerBillingOption in requestPurchase
+
+
+
+
+
+
+
+ ⚠️ Important: The external transaction token MUST be
+ reported to Google Play within 24 hours using the{' '}
+ externaltransactions.createexternaltransaction API.
+ Failure to report tokens may result in account suspension.
+
+ Registers a listener for User Choice Billing events. This listener is
+ only triggered when the user selects alternative billing instead of
+ Google Play billing.
+
+
+
+ {{
+ typescript: (
+ {`import { userChoiceBillingListenerAndroid } from 'expo-iap';
+
+const subscription = userChoiceBillingListenerAndroid(async (details) => {
+ console.log('User chose alternative billing');
+ console.log('Products:', details.products);
+ console.log('Token:', details.externalTransactionToken);
+
+ // Process payment with your backend
+ const paymentResult = await processPaymentWithBackend({
+ products: details.products,
+ token: details.externalTransactionToken,
+ });
+
+ if (paymentResult.success) {
+ // Backend should report token to Google Play within 24 hours
+ grantUserAccess(details.products);
+ }
+});
+
+// Cleanup when done
+subscription.remove();`}
+ ),
+ kotlin: (
+ {`import dev.hyo.openiap.UserChoiceBillingDetails
+
+// Using Flow
+lifecycleScope.launch {
+ openIapStore.userChoiceBillingEvents.collect { details ->
+ println("User chose alternative billing")
+ println("Products: \${details.products}")
+ println("Token: \${details.externalTransactionToken}")
+
+ // Process payment with your backend
+ val paymentResult = processPaymentWithBackend(
+ products = details.products,
+ token = details.externalTransactionToken
+ )
+
+ if (paymentResult.success) {
+ // Backend should report token to Google Play within 24 hours
+ grantUserAccess(details.products)
+ }
+ }
+}
+
+// Or with callback
+openIapStore.setUserChoiceBillingListener { details ->
+ println("User chose alternative billing for: \${details.products}")
+}`}
+ ),
+ kmp: (
+ {`import io.github.hyochan.kmpiap.KmpIAP
+
+val kmpIAP = KmpIAP()
+
+// Using Flow
+lifecycleScope.launch {
+ kmpIAP.userChoiceBillingEvents.collect { details ->
+ println("User chose alternative billing")
+ println("Products: \${details.products}")
+ println("Token: \${details.externalTransactionToken}")
+
+ // Process payment with your backend
+ val paymentResult = processPaymentWithBackend(
+ products = details.products,
+ token = details.externalTransactionToken
+ )
+
+ if (paymentResult.success) {
+ // Backend should report token to Google Play within 24 hours
+ grantUserAccess(details.products)
+ }
+ }
+}
+
+// Or with callback
+kmpIAP.setUserChoiceBillingListener { details ->
+ println("User chose alternative billing for: \${details.products}")
+}`}
+ ),
+ dart: (
+ {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
+
+// Android only - will not fire on iOS
+final subscription = FlutterInappPurchase.userChoiceBillingAndroid.listen((details) async {
+ print('User chose alternative billing');
+ print('Products: \${details?.products}');
+ print('Token: \${details?.externalTransactionToken}');
+
+ // Process payment with your backend
+ final paymentResult = await processPaymentWithBackend(
+ products: details!.products,
+ token: details.externalTransactionToken,
+ );
+
+ if (paymentResult.success) {
+ // Backend should report token to Google Play within 24 hours
+ grantUserAccess(details.products);
+ }
+});
+
+// Cleanup when done
+subscription.cancel();`}
+ ),
+ }}
+
+
+
Event Payload
+
+ {{
+ typescript: (
+ {`interface UserChoiceBillingDetails {
+ externalTransactionToken: string;
+ products: string[];
+}`}
+ ),
+ swift: (
+ {`// Android only - not available on iOS`}
+ ),
+ kotlin: (
+ {`data class UserChoiceBillingDetails(
+ val externalTransactionToken: String,
+ val products: List
+)`}
+ ),
+ kmp: (
+ {`data class UserChoiceBillingDetails(
+ val externalTransactionToken: String,
+ val products: List
+)`}
+ ),
+ dart: (
+ {`class UserChoiceBillingDetails {
+ final String externalTransactionToken;
+ final List products;
+}`}
+ ),
+ }}
+
+
+ externalTransactionToken - Token that must be reported
+ to Google Play within 24 hours
+
+ products - List of product IDs selected by the user
+
+
+
Handling User Choice Billing
+
+
+ Receive UserChoiceBillingDetails via listener
+
+
Process payment with your backend payment system
+
Send the external transaction token to your backend
+
+ Backend reports token to Google Play within 24 hours (required for
+ compliance)
+
+
Grant user access to purchased content
+
+
+
+
+ ⚠️ Important: The external transaction token MUST be
+ reported to Google Play within 24 hours. Failure to report tokens may
+ result in account suspension. It is strongly recommended to handle
+ token reporting on your backend server for reliability and security.
+
+
+
+
Flow Comparison
+
+ When using User Choice Billing mode, there are two possible flows
+ depending on user selection:
+
+
+
+ Google Play selected - Standard{' '}
+ PurchaseUpdated event fires (handle normally)
+
+
+ Alternative billing selected -{' '}
+ UserChoiceBillingAndroid event fires (handle with your
+ payment system)
+
+ 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 can be
+ purchased directly via the standard{' '}
+
+ requestPurchase()
+ {' '}
+ flow. The deprecated{' '}
+
+ requestPurchaseOnPromotedProductIOS()
+ {' '}
+ API is no longer needed.
+
+
+
+ );
+}
+
+export default PromotedProductListenerIOS;
diff --git a/packages/docs/src/pages/docs/events/purchase-error-listener.tsx b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx
new file mode 100644
index 000000000..1fbb0dba4
--- /dev/null
+++ b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx
@@ -0,0 +1,238 @@
+import { Link } from 'react-router-dom';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function PurchaseErrorListener() {
+ useScrollToHash();
+
+ return (
+
+
+
purchaseErrorListener
+
Fired when a purchase fails or is cancelled by the user.
+ Finish transaction with{' '}
+ finishTransaction{' '}
+ (handles acknowledgment on both platforms)
+
+
Update application state
+
+
+ );
+}
+
+export default PurchaseUpdatedListener;
diff --git a/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx
new file mode 100644
index 000000000..eb88ec3e4
--- /dev/null
+++ b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx
@@ -0,0 +1,185 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function SubscriptionBillingIssueListener() {
+ useScrollToHash();
+
+ return (
+
+
+
subscriptionBillingIssueListener
+
+ Fired when an active subscription enters a state that needs user
+ attention because of a payment problem — card declined, expired payment
+ method, billing retry, or grace period. Unifies StoreKit 2{' '}
+ Message.billingIssue (iOS 18+) and Play Billing{' '}
+ Purchase.isSuspended (Play Billing Library 8.1+) under a
+ single cross-platform stream. Silent no-op on platforms that cannot emit
+ (tvOS, watchOS, visionOS, macOS, Meta Horizon).
+
+ The emitted{' '}
+
+ Purchase
+ {' '}
+ is a regular subscription payload — use productId,{' '}
+ purchaseToken, and platform fields to prompt the user to
+ update payment. Play deduplicates by purchaseToken per
+ session; iOS fires per Message delivery.
+
+
+
+ Example
+
+
+ {{
+ typescript: (
+ {`// expo-iap
+import { subscriptionBillingIssueListener } from 'expo-iap';
+// Same API in react-native-iap:
+// import { subscriptionBillingIssueListener } from 'react-native-iap';
+
+const subscription = subscriptionBillingIssueListener((purchase) => {
+ console.log('Billing issue on', purchase.productId);
+ // Surface a "Update payment method" prompt and link the user to
+ // the platform's subscription management UI.
+ showBillingIssueBanner(purchase);
+});
+
+// Cleanup when the screen unmounts
+subscription.remove();
+
+// --- Or via the useIAP() hook (also exported from react-native-iap) ---
+import { useIAP } from 'expo-iap';
+
+function BillingIssueGate() {
+ useIAP({
+ onSubscriptionBillingIssue: (purchase) => {
+ showBillingIssueBanner(purchase);
+ },
+ });
+ return null;
+}`}
+ ),
+ swift: (
+ {`import OpenIap
+
+// iOS 18+ only — no-op on older versions
+let subscription = OpenIapModule.shared.subscriptionBillingIssueListener { purchase in
+ print("Billing issue on \\(purchase.productId)")
+ Task { await showBillingIssueBanner(purchase) }
+}
+
+// Cleanup when the view disappears
+subscription.remove()`}
+ ),
+ kotlin: (
+ {`import dev.hyo.openiap.OpenIapStore
+
+val openIapStore = OpenIapStore(context)
+
+// Play Billing Library 8.1+
+val listener: (Purchase) -> Unit = { purchase ->
+ println("Billing issue on \${purchase.productId}")
+ showBillingIssueBanner(purchase)
+}
+openIapStore.addSubscriptionBillingIssueListener(listener)
+
+// Cleanup when the view disappears
+openIapStore.removeSubscriptionBillingIssueListener(listener)`}
+ ),
+ kmp: (
+ {`import io.github.hyochan.kmpiap.KmpIAP
+
+val kmpIAP = KmpIAP()
+
+// Play Billing 8.1+ on Android, iOS 18+ on Apple targets
+lifecycleScope.launch {
+ kmpIAP.subscriptionBillingIssueListener.collect { purchase ->
+ println("Billing issue on \${purchase.productId}")
+ showBillingIssueBanner(purchase)
+ }
+}`}
+ ),
+ dart: (
+ {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
+
+final subscription =
+ FlutterInappPurchase.subscriptionBillingIssueListener.listen((purchase) {
+ debugPrint('Billing issue on \${purchase.productId}');
+ showBillingIssueBanner(purchase);
+ });
+
+// Cleanup when the widget disposes
+subscription.cancel();`}
+ ),
+ gdscript: (
+ {`iap.subscription_billing_issue.connect(_on_billing_issue)
+
+func _on_billing_issue(purchase: Purchase):
+ print("Billing issue on %s" % purchase.product_id)
+ show_billing_issue_banner(purchase)
+
+# Cleanup when leaving the scene
+func _exit_tree():
+ iap.subscription_billing_issue.disconnect(_on_billing_issue)`}
+ ),
+ }}
+
+
+
+ See{' '}
+
+ Subscription Billing Issue feature guide
+ {' '}
+ for platform coverage, signal sources, and UX recommendations.
+
+
);
}
diff --git a/packages/docs/src/pages/docs/apis/debugging.tsx b/packages/docs/src/pages/docs/features/debugging.tsx
similarity index 75%
rename from packages/docs/src/pages/docs/apis/debugging.tsx
rename to packages/docs/src/pages/docs/features/debugging.tsx
index 266da6919..42f35777a 100644
--- a/packages/docs/src/pages/docs/apis/debugging.tsx
+++ b/packages/docs/src/pages/docs/features/debugging.tsx
@@ -7,16 +7,16 @@ import SEO from '../../../components/SEO';
import TLDRBox from '../../../components/TLDRBox';
import { useScrollToHash } from '../../../hooks/useScrollToHash';
-function DebuggingAPIs() {
+function Debugging() {
useScrollToHash();
return (
Debugging & Logging
@@ -94,11 +94,18 @@ OpenIapLog.enable(false)`}
Root Cause
- Google Play Billing API's Purchase object does NOT
- include basePlanId information. When a subscription group
- has multiple base plans (weekly, monthly, yearly), there is no way to
- determine which specific plan was purchased from the client-side{' '}
- Purchase object.
+ Google Play Billing API's{' '}
+
+ Purchase
+ {' '}
+ object does NOT include basePlanId information. When a
+ subscription group has multiple base plans (weekly, monthly, yearly),
+ there is no way to determine which specific plan was purchased from
+ the client-side{' '}
+
+ Purchase
+ {' '}
+ object.
Note: This is a fundamental limitation of Google
Play Billing API, not a bug in this library. The{' '}
- Purchase object from Google simply does not include{' '}
- basePlanId information.
+
+ Purchase
+ {' '}
+ object from Google simply does not include basePlanId{' '}
+ information.
+
);
}
-export default DebuggingAPIs;
+export default Debugging;
diff --git a/packages/docs/src/pages/docs/features/discount.tsx b/packages/docs/src/pages/docs/features/discount.tsx
index 2bdd0fd51..8a6b8c217 100644
--- a/packages/docs/src/pages/docs/features/discount.tsx
+++ b/packages/docs/src/pages/docs/features/discount.tsx
@@ -1,3 +1,4 @@
+import { Link } from 'react-router-dom';
import AnchorLink from '../../../components/AnchorLink';
import CodeBlock from '../../../components/CodeBlock';
import LanguageTabs from '../../../components/LanguageTabs';
@@ -33,10 +34,9 @@ function Discount() {
Standardized Types: For cross-platform development,
- use the new{' '}
- DiscountOffer type
- which provides a unified interface with platform-specific fields via
- suffixes (e.g., offerTokenAndroid).
+ use the new DiscountOffer{' '}
+ type which provides a unified interface with platform-specific fields
+ via suffixes (e.g., offerTokenAndroid).
+
);
}
diff --git a/packages/docs/src/pages/docs/features/refund.tsx b/packages/docs/src/pages/docs/features/refund.tsx
new file mode 100644
index 000000000..21b2076f3
--- /dev/null
+++ b/packages/docs/src/pages/docs/features/refund.tsx
@@ -0,0 +1,533 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import PlatformTabs from '../../../components/PlatformTabs';
+import SEO from '../../../components/SEO';
+import TLDRBox from '../../../components/TLDRBox';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function Refund() {
+ useScrollToHash();
+
+ return (
+
+
+
Refund
+
+ Handle refunds initiated by users or store-side actions. iOS supports
+ in-app refund requests via StoreKit 2, while Android refunds are
+ store-driven and require server-side detection.
+
+ Android: No client-side refund API. Auto-refunded
+ after 3 days if not acknowledged
+
+
+ Server-side: Subscribe to App Store Server
+ Notifications V2 (Apple) and Real-time Developer Notifications
+ (Google) to react to refunds
+
+
+ Critical: Always revoke entitlements when a refund
+ is detected
+
+
+
+
+
+
+ Platform Differences
+
+
+
+
+
Platform
+
Client API
+
Detection
+
+
+
+
+
iOS
+
+ beginRefundRequestIOS (iOS 15+)
+
+
+ App Store Server Notifications V2 (REFUND,{' '}
+ REVOKE)
+
+
+
+
Android
+
None — store-driven
+
+ Real-time Developer Notifications —{' '}
+ voidedPurchaseNotification for one-time products,{' '}
+ subscriptionNotification.SUBSCRIPTION_REVOKED for
+ subscriptions — plus server-side reconciliation via the Voided
+ Purchases API
+
+ iOS lets users request refunds directly from inside your app
+ using StoreKit 2's refund sheet. The system handles the refund
+ flow; your app receives the result via the returned status
+ string.
+
+
+
Requires iOS 15+
+
Not available on tvOS
+
+ The actual refund decision is made by Apple — the API only
+ initiates the request
+
+
+ For detection of approved refunds, use App Store Server
+ Notifications V2
+
+
+
+
+ beginRefundRequestIOS
+
+
+ Present the refund request sheet for a previously purchased
+ product.
+
+ Apple sends a server-to-server notification when a refund is
+ approved. Subscribe to handle revocation reliably — the in-app
+ status is just the request, not the final outcome.
+
+
+
+
+
notificationType
+
Meaning
+
+
+
+
+
+ REFUND
+
+
Apple refunded the user
+
+
+
+ REFUND_DECLINED
+
+
Refund was declined
+
+
+
+ REVOKE
+
+
+ Family Sharing access revoked (treat like a refund)
+
+
+
+
+ CONSUMPTION_REQUEST
+
+
+ Apple wants consumption data to decide on a refund —
+ respond within 12 hours
+
+
+
+
+ {`// Server webhook handler (Node.js).
+// In App Store Server Notifications V2, signedTransactionInfo is itself a
+// signed JWS string — verify and decode it to read its fields.
+app.post('/webhooks/apple', async (req, res) => {
+ const { signedPayload } = req.body;
+ const decoded = await verifyAndDecodeJWS(signedPayload);
+
+ if (decoded.notificationType === 'REFUND' || decoded.notificationType === 'REVOKE') {
+ const transactionInfo = await verifyAndDecodeJWS(
+ decoded.data.signedTransactionInfo,
+ );
+ await revokeEntitlement(transactionInfo.transactionId);
+ }
+
+ res.sendStatus(200);
+});`}
+
+
+ Testing
+
+
+
+ beginRefundRequestIOS can be exercised in
+ sandbox but the sheet may show limited UI
+
+
+ Use StoreKit's "Refund" command in Xcode StoreKit
+ Configuration to simulate refunds
+
+
+ Configure the sandbox URL for App Store Server Notifications
+ in App Store Connect
+
+
+ >
+ ),
+ android: (
+ <>
+
+ Overview
+
+
+ Google Play does not expose an in-app refund API. Refunds
+ originate from the Play Store, support requests, or
+ developer-initiated actions in the Play Console. Two paths
+ trigger an effective refund:
+
+
+
+ Auto-refund after 3 days — Google
+ automatically refunds purchases that aren't acknowledged
+ within 3 days. Always call finishTransaction{' '}
+ immediately after server verification.
+
+
+ User or Play Console refund — initiated via
+ Play Store, Google support, or your Play Console
+
+
+
+
+
+ Critical: If you don't acknowledge an
+ Android purchase within 3 days, Google will auto-refund the
+ user. See Purchase{' '}
+ for the acknowledgment flow.
+
+
+
+
+ Real-time Developer Notifications
+
+
+ Subscribe to Pub/Sub-backed Real-time Developer Notifications
+ (RTDN) to receive refund events. There is no client API to
+ query refund state directly.
+
+ If you cannot run a webhook, poll the{' '}
+
+ Google Play Voided Purchases API
+
+ . It returns purchases voided in the last 30 days (or 60 days
+ with extended access).
+
+
+
+ Testing
+
+
+
+ Use license testers and the "Refund" action in Play Console
+ to simulate refunds
+
+
RTDN works in test tracks once Pub/Sub is wired up
+
+ To test the 3-day auto-refund, intentionally skip{' '}
+ finishTransaction and wait
+
StoreKit.Message.Reason.billingIssue
+
+ Mac Catalyst 18.0+
Push, while app is active
-
Mac Catalyst 18.0+
Android (Play)
Purchase.isSuspended
+
+ Play Billing Library 8.1+
Poll via getAvailablePurchases or on{' '}
onPurchasesUpdated
-
Play Billing Library 8.1+
Android (Meta Horizon)
-
Not available
+
+ Not available
+
+ Billing 7.0 compat SDK
+
Never fires (silent no-op)
-
N/A — Billing 7.0 compat SDK
-
macOS / tvOS / watchOS / visionOS
+
+ macOS
+
+ tvOS
+
+ watchOS
+
+ visionOS
+
StoreKit.Message not available
Never fires
-
N/A
@@ -115,11 +128,14 @@ function SubscriptionBillingIssue() {
When this event fires, route the user to the platform subscription
- center via deepLinkToSubscriptions() so they can update
- their payment method. Do not re-grant entitlements on
- the assumption the subscription is still active — Play suspends
- entitlement for these purchases, and iOS will re-emit the message
- until the billing issue is resolved.
+ center via{' '}
+
+ deepLinkToSubscriptions()
+ {' '}
+ so they can update their payment method. Do not{' '}
+ re-grant entitlements on the assumption the subscription is still
+ active — Play suspends entitlement for these purchases, and iOS will
+ re-emit the message until the billing issue is resolved.
@@ -206,9 +222,12 @@ kmpIapInstance.subscriptionBillingIssueListener
On Android, the native SDK tracks emitted purchase tokens per session
so the event fires once per affected purchase even if the app
polls getAvailablePurchases repeatedly. The dedupe set is
- only cleared on endConnection() or app restart — a
- purchase that exits suspension and re-enters within the same session
- will
+ only cleared on{' '}
+
+ endConnection()
+ {' '}
+ or app restart — a purchase that exits suspension and re-enters within
+ the same session will
not re-emit until the next reconnect or process
restart.
@@ -219,6 +238,54 @@ kmpIapInstance.subscriptionBillingIssueListener
.inBillingRetryPeriod or .inGracePeriod.
+
+
+
+ Native References
+
+
);
}
diff --git a/packages/docs/src/pages/docs/features/subscription/index.tsx b/packages/docs/src/pages/docs/features/subscription/index.tsx
index 3e187f745..3ff4f906f 100644
--- a/packages/docs/src/pages/docs/features/subscription/index.tsx
+++ b/packages/docs/src/pages/docs/features/subscription/index.tsx
@@ -69,7 +69,11 @@ function Subscription() {
Subscription offers are required when
purchasing. You must pass subscriptionOffers with
- offer tokens from fetchProducts().
+ offer tokens from{' '}
+
+ fetchProducts()
+
+ .
@@ -741,7 +745,10 @@ suspend fun purchaseSubscription(subscriptionId: String) {
Android requires explicit specification of subscription offers
when purchasing. Each offer is identified by an{' '}
offerToken obtained from{' '}
- fetchProducts().
+
+ fetchProducts()
+
+ .
@@ -1233,10 +1240,12 @@ suspend fun purchaseSubscription(subscriptionId: String) {
{' '}
returned by Google Play Billing does NOT{' '}
include basePlanId. This means{' '}
- getActiveSubscriptions() and purchase callbacks
- cannot reliably determine which specific plan was purchased
- within a subscription group. See{' '}
-
+
+ getActiveSubscriptions()
+ {' '}
+ and purchase callbacks cannot reliably determine which
+ specific plan was purchased within a subscription group. See{' '}
+
detailed limitation and solutions
.
@@ -1253,15 +1262,21 @@ suspend fun purchaseSubscription(subscriptionId: String) {
basePlanId
- Purchase object (from purchase callbacks) - ❌
- Does NOT contain basePlanId
+
+ Purchase
+ {' '}
+ object (from purchase callbacks) - ❌ Does NOT contain{' '}
+ basePlanId
When a subscription group has multiple base plans (weekly,
monthly, yearly), there is no way to determine which specific
- plan was purchased from the client-side Purchase{' '}
+ plan was purchased from the client-side{' '}
+
+ Purchase
+ {' '}
object alone.
@@ -1551,7 +1566,7 @@ func _on_purchase_success(purchase: PurchaseAndroid) -> void:
IAPKit
{' '}
which handles all the complexity for you. Use{' '}
-
+
verifyPurchaseWithProvider
{' '}
to verify purchases with minimal setup.
@@ -1984,7 +1999,10 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void:
Subscription remains valid until Day 30 (end of billing period)
- getAvailablePurchases() still returns this purchase
+
+ getAvailablePurchases()
+ {' '}
+ still returns this purchase
For the complete list of subscription state values and
expiration reasons, see{' '}
-
+
SubscriptionStatusIOS
{' '}
in the Types reference.
@@ -3314,6 +3339,54 @@ func manage_subscriptions() -> void:
+
+
+
+ Native References
+
+
+
+ IAPKit
+ {' '}
+ is a managed receipt-validation service for App Store and Google Play
+ purchases. Instead of running your own backend that talks to Apple's
+ App Store Server API and Google Play Developer API, you forward the
+ JWS / purchase token to IAPKit and get a normalized verification
+ response — so one-time in-app purchases can't be faked, replayed, or
+ tampered with.
+
+
+
Why use it
+
+
+ Cross-store, one schema — same{' '}
+
+ VerifyPurchaseWithProviderResult
+ {' '}
+ shape for Apple and Google. No per-platform JSON parsing.
+
+
+ Fraud-proof — verifies Apple JWS signatures and
+ queries Google Play's authoritative subscription/purchase state on
+ the server, blocking forged receipts and replay attacks.
+
+
+ Entitlement state, not raw receipts — IAPKit
+ collapses raw store data into a single state field (
+ entitled, pending, canceled,{' '}
+ expired, refunded,{' '}
+ inauthentic, etc.) so your client and server can act on
+ a single value.
+
+
+ No backend boilerplate — no service account JSON,
+ no App Store private key rotation, no webhook plumbing required to
+ get started.
+
+
+
+
When to roll your own instead
+
+
+ You have strict data-residency requirements that disallow sending
+ purchase tokens to a third-party.
+
+
+ You already operate a hardened receipt-validation service and don't
+ want another vendor in the path.
+
+
);
}
-export default ValidationAPIs;
+export default Validation;
diff --git a/packages/docs/src/pages/docs/getting-started.tsx b/packages/docs/src/pages/docs/getting-started.tsx
new file mode 100644
index 000000000..386b26710
--- /dev/null
+++ b/packages/docs/src/pages/docs/getting-started.tsx
@@ -0,0 +1,481 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../components/AnchorLink';
+import CodeBlock from '../../components/CodeBlock';
+import LanguageTabs from '../../components/LanguageTabs';
+import SEO from '../../components/SEO';
+import { useScrollToHash } from '../../hooks/useScrollToHash';
+
+function GettingStarted() {
+ useScrollToHash();
+
+ return (
+
+
+
Getting Started
+
+ OpenIAP is a unified spec for in-app purchases on Apple, Google, and
+ Meta Horizon. One GraphQL schema generates type-safe SDKs for
+ TypeScript, Swift, Kotlin, Dart, and GDScript — so the same purchase
+ flow works across every framework you ship in.
+
+
+
+ This page is a five-minute walkthrough. If you'd rather jump straight
+ into your stack, head to Framework Setup.
+
+
+
+
+ 1. Configure the store
+
+
+ Every framework wraps the same store APIs, so the platform setup comes
+ first. Finish these before installing any SDK:
+
+ The four-step flow below is the same on every framework — only the
+ imports differ. Read{' '}
+ Features → Purchase for a
+ full walkthrough with verification, error handling, and
+ consumable/non-consumable nuances.
+
+
+
+ {{
+ typescript: (
+ {`import {
+ initConnection,
+ fetchProducts,
+ requestPurchase,
+ finishTransaction,
+ purchaseUpdatedListener,
+ verifyPurchase,
+} from 'expo-iap';
+
+// 1. Open the store connection on app start.
+await initConnection();
+
+// 2. Fetch products by SKU.
+const products = await fetchProducts({
+ skus: ['com.app.premium'],
+ type: 'in-app',
+});
+
+// 3. Listen for purchase results — requestPurchase is event-based.
+purchaseUpdatedListener(async (purchase) => {
+ const { isValid } = await verifyPurchase({
+ purchase,
+ serverUrl: 'https://your-server.com/api/verify',
+ });
+ if (!isValid) return;
+
+ await grantEntitlement(purchase.productId);
+ await finishTransaction({ purchase, isConsumable: false });
+});
+
+// 4. Initiate a purchase.
+await requestPurchase({
+ request: {
+ apple: { sku: 'com.app.premium' },
+ google: { skus: ['com.app.premium'] },
+ },
+ type: 'in-app',
+});`}
+ ),
+ swift: (
+ {`import OpenIap
+
+let store = OpenIapModule.shared
+
+// 1. Open the store connection on app start.
+try await store.initConnection()
+
+// 2. Fetch products by SKU.
+let products = try await store.fetchProducts(
+ ProductRequest(skus: ["com.app.premium"], type: .inApp)
+)
+
+// 3. Listen for purchase results — requestPurchase is event-based.
+Task {
+ for await purchase in store.purchaseUpdates {
+ // Verify on your backend, grant entitlement, then finish.
+ try await store.finishTransaction(purchase, isConsumable: false)
+ }
+}
+
+// 4. Initiate a purchase.
+try await store.requestPurchase(
+ RequestPurchaseProps(
+ request: RequestPurchasePropsByPlatforms(
+ apple: RequestPurchaseIosProps(sku: "com.app.premium")
+ ),
+ type: .inApp
+ )
+)`}
+ ),
+ kotlin: (
+ {`import dev.hyo.openiap.store.OpenIapStore
+import dev.hyo.openiap.*
+
+val store = OpenIapStore(context)
+
+// 1. Open the store connection on app start.
+store.initConnection(null)
+
+// 2. Fetch products by SKU.
+val products = store.fetchProducts(
+ ProductRequest(
+ skus = listOf("com.app.premium"),
+ type = ProductQueryType.InApp
+ )
+)
+
+// 3. Listen for purchase results.
+scope.launch {
+ store.purchaseUpdates.collect { purchase ->
+ // Verify on your backend, grant entitlement, then finish.
+ store.finishTransaction(purchase, isConsumable = false)
+ }
+}
+
+// 4. Initiate a purchase.
+store.requestPurchase(
+ RequestPurchaseProps(
+ request = RequestPurchasePropsByPlatforms(
+ google = RequestPurchaseAndroidProps(skus = listOf("com.app.premium"))
+ ),
+ type = ProductQueryType.InApp
+ )
+)`}
+ ),
+ kmp: (
+ {`import io.github.hyochan.kmpiap.kmpIAP
+import io.github.hyochan.kmpiap.types.*
+
+// 1. Open the store connection on app start.
+kmpIAP.initConnection()
+
+// 2. Fetch products by SKU.
+val products = kmpIAP.fetchProducts(
+ ProductRequest(
+ skus = listOf("com.app.premium"),
+ type = ProductQueryType.InApp
+ )
+)
+
+// 3. Listen for purchase results.
+scope.launch {
+ kmpIAP.purchaseUpdates.collect { purchase ->
+ // Verify on your backend, grant entitlement, then finish.
+ kmpIAP.finishTransaction(purchase, isConsumable = false)
+ }
+}
+
+// 4. Initiate a purchase.
+kmpIAP.requestPurchase(
+ RequestPurchaseProps(
+ request = RequestPurchasePropsByPlatforms(
+ apple = RequestPurchaseIosProps(sku = "com.app.premium"),
+ google = RequestPurchaseAndroidProps(skus = listOf("com.app.premium"))
+ ),
+ type = ProductQueryType.InApp
+ )
+)
+
+// --- Or via the DSL API ---
+// Same flow, but platform options live inside ios { } / android { }
+// blocks. The call returns the resulting Purchase, which you pipe through
+// .toPurchaseInput() into finishTransaction.
+val purchase = kmpIAP.requestPurchase {
+ ios {
+ sku = "com.app.premium"
+ quantity = 1
+ }
+ android {
+ skus = listOf("com.app.premium")
+ }
+}
+
+kmpIAP.finishTransaction(
+ purchase = purchase.toPurchaseInput(),
+ isConsumable = false
+)`}
+ ),
+ dart: (
+ {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
+
+final iap = FlutterInappPurchase.instance;
+
+// 1. Open the store connection on app start.
+await iap.initConnection();
+
+// 2. Fetch products by SKU.
+final FetchProductsResult result = await iap.fetchProducts(
+ skus: ['com.app.premium'],
+ type: ProductQueryType.InApp,
+);
+
+// 3. Listen for purchase results.
+FlutterInappPurchase.purchaseUpdatedStream.listen((purchase) async {
+ if (purchase == null) return;
+ // Verify on your backend, grant entitlement, then finish.
+ await iap.finishTransaction(purchase, isConsumable: false);
+});
+
+// 4. Initiate a purchase.
+await iap.requestPurchase(
+ RequestPurchaseProps(
+ request: RequestPurchasePropsByPlatforms(
+ apple: RequestPurchaseIosProps(sku: 'com.app.premium'),
+ google: RequestPurchaseAndroidProps(skus: ['com.app.premium']),
+ ),
+ type: ProductQueryType.InApp,
+ ),
+);
+
+// --- Or via the builder DSL ---
+// requestPurchaseWithBuilder accepts a build closure; assign type and
+// platform-specific fields fluently with cascade operators.
+await iap.requestPurchaseWithBuilder(
+ build: (builder) {
+ builder
+ ..type = ProductQueryType.InApp
+ ..android.skus = ['com.app.premium']
+ ..ios.sku = 'com.app.premium';
+ },
+);`}
+ ),
+ gdscript: (
+ {`# 1. Open the store connection on app start.
+await iap.init_connection()
+
+# 2. Fetch products by SKU.
+var request = ProductRequest.new()
+request.skus = ["com.app.premium"]
+request.type = ProductQueryType.IN_APP
+var products = await iap.fetch_products(request)
+
+# 3. Listen for purchase results.
+iap.purchase_updated.connect(func(purchase):
+ # Verify on your backend, grant entitlement, then finish.
+ await iap.finish_transaction(purchase, false)
+)
+
+# 4. Initiate a purchase.
+var props = RequestPurchaseProps.new()
+props.request = RequestPurchasePropsByPlatforms.new()
+props.request.apple = RequestPurchaseIosProps.new()
+props.request.apple.sku = "com.app.premium"
+props.type = ProductQueryType.IN_APP
+await iap.request_purchase(props)`}
+ ),
+ }}
+
+
+
+
+
+ 4. Key concepts
+
+
+ Two background reads make every framework guide easier to follow —
+ skim them before you wire anything into production.
+
+
+
+ Ecosystem — how OpenIAP, the
+ native packages (Apple / Google), and each framework SDK fit
+ together. Read this first if you're choosing a stack.
+
+
+ Life Cycle — when to call{' '}
+ initConnection, where to mount listeners, and when to
+ call finishTransaction. Getting this wrong is the #1
+ cause of "purchase succeeded but the user didn't get the
+ entitlement" reports, so read it once even if you skip everything
+ else.
+
+
+
+
+
+
+ 5. How the docs are organized
+
+
+ The sidebar groups content by intent. Once you know which group fits
+ the question you're answering, navigation becomes muscle memory.
+
+
+ Features
+ {' '}
+ — task-oriented walkthroughs, not reference. Each page covers a real
+ flow end-to-end (Purchase,{' '}
+ Subscription,{' '}
+ Refund,{' '}
+ Validation,{' '}
+
+ Offer Code Redemption
+
+ , …) with verification, edge cases, and platform notes. Open a
+ Features page when you're shipping a flow, not just calling a
+ function.
+
+
+
+ APIs
+ {' '}
+ — flat reference, one page per function (initConnection
+ , fetchProducts, requestPurchase, …).
+ Cross-platform symbols live at the root; iOS- and Android-only
+ symbols are grouped under{' '}
+ iOS Specific /{' '}
+ Android Specific.
+ Open a function page when you need its exact signature, params, and
+ a copy-pasteable example.
+
+
+
+ Types
+ {' '}
+ — flat reference, one page per type (
+
+ Product
+
+ ,{' '}
+
+ Purchase
+
+ ,{' '}
+
+ RequestPurchaseProps
+
+ , …). Same iOS / Android grouping as APIs. Field tables auto-link to
+ related types so you can chase a shape without leaving the docs.
+
+
+
+ Events &{' '}
+ Errors
+ {' '}
+ — listener patterns and the unified{' '}
+
+ PurchaseError
+ {' '}
+ codes that every SDK normalizes to.
+
+
+
+ Rule of thumb: "How does this function work?" → APIs.
+ "What does this object look like?" → Types. "How do I ship
+ subscription upgrades?" → Features.
+
+ );
+}
+
+export default GettingStarted;
diff --git a/packages/docs/src/pages/docs/index.tsx b/packages/docs/src/pages/docs/index.tsx
index 189d3ada1..b68c0515c 100644
--- a/packages/docs/src/pages/docs/index.tsx
+++ b/packages/docs/src/pages/docs/index.tsx
@@ -1,28 +1,96 @@
import { useState, useEffect } from 'react';
-import { Route, Routes, Navigate, NavLink } from 'react-router-dom';
+import {
+ Route,
+ Routes,
+ Navigate,
+ NavLink,
+ useLocation,
+} from 'react-router-dom';
import { MenuDropdown } from '../../components/MenuDropdown';
+import GettingStarted from './getting-started';
import Ecosystem from './ecosystem';
import LifeCycle from './lifecycle';
import Subscription from './lifecycle/subscription';
import TypesIndex from './types/index';
import TypesProduct from './types/product';
+import TypesSubscriptionProduct from './types/subscription-product';
+import TypesStorefront from './types/storefront';
import TypesPurchase from './types/purchase';
-import TypesRequest from './types/request';
-import TypesAlternative from './types/alternative';
-import TypesVerification from './types/verification';
-import TypesIOS from './types/ios';
-import TypesAndroid from './types/android';
-import TypesOffer from './types/offer';
+import TypesActiveSubscription from './types/active-subscription';
+import TypesProductRequest from './types/product-request';
+import TypesRequestPurchaseProps from './types/request-purchase-props';
+import TypesAlternativeBillingTypes from './types/alternative-billing-types';
+import TypesBillingPrograms from './types/billing-programs';
+import TypesExternalPurchaseLink from './types/external-purchase-link';
+import TypesVerifyPurchase from './types/verify-purchase';
+import TypesVerifyPurchaseWithProviderProps from './types/verify-purchase-with-provider-props';
+import TypesVerifyPurchaseWithProviderResult from './types/verify-purchase-with-provider-result';
+import TypesDiscountOfferIOS from './types/ios/discount-offer-ios';
+import TypesDiscountIOS from './types/ios/discount-ios';
+import TypesSubscriptionPeriodIOS from './types/ios/subscription-period-ios';
+import TypesPaymentModeIOS from './types/ios/payment-mode-ios';
+import TypesSubscriptionStatusIOS from './types/ios/subscription-status-ios';
+import TypesAppTransactionIOS from './types/ios/app-transaction-ios';
+import TypesRenewalInfoIOS from './types/ios/renewal-info-ios';
+import TypesOneTimePurchaseOfferDetailAndroid from './types/android/one-time-purchase-offer-detail-android';
+import TypesSubscriptionOfferAndroid from './types/android/subscription-offer-android';
+import TypesPricingPhaseAndroid from './types/android/pricing-phase-android';
+import TypesDiscountOffer from './types/discount-offer';
+import TypesSubscriptionOffer from './types/subscription-offer';
import APIsIndex from './apis/index';
-import APIsConnection from './apis/connection';
-import APIsProducts from './apis/products';
-import APIsPurchase from './apis/purchase';
-import APIsSubscription from './apis/subscription';
-import APIsValidation from './apis/validation';
-import APIsIOS from './apis/ios';
-import APIsAndroid from './apis/android';
-import APIsDebugging from './apis/debugging';
+import APIsInitConnection from './apis/init-connection';
+import APIsEndConnection from './apis/end-connection';
+import APIsFetchProducts from './apis/fetch-products';
+import APIsGetAvailablePurchases from './apis/get-available-purchases';
+import APIsRequestPurchase from './apis/request-purchase';
+import APIsFinishTransaction from './apis/finish-transaction';
+import APIsRestorePurchases from './apis/restore-purchases';
+import APIsGetStorefront from './apis/get-storefront';
+import APIsGetActiveSubscriptions from './apis/get-active-subscriptions';
+import APIsHasActiveSubscriptions from './apis/has-active-subscriptions';
+import APIsDeepLinkToSubscriptions from './apis/deep-link-to-subscriptions';
+import APIsValidateReceipt from './apis/validate-receipt';
+import APIsClearTransactionIOS from './apis/ios/clear-transaction-ios';
+import APIsGetPendingTransactionsIOS from './apis/ios/get-pending-transactions-ios';
+import APIsGetAllTransactionsIOS from './apis/ios/get-all-transactions-ios';
+import APIsSyncIOS from './apis/ios/sync-ios';
+import APIsGetStorefrontIOS from './apis/ios/get-storefront-ios';
+import APIsGetPromotedProductIOS from './apis/ios/get-promoted-product-ios';
+import APIsIsEligibleForIntroOfferIOS from './apis/ios/is-eligible-for-intro-offer-ios';
+import APIsSubscriptionStatusIOS from './apis/ios/subscription-status-ios';
+import APIsCurrentEntitlementIOS from './apis/ios/current-entitlement-ios';
+import APIsLatestTransactionIOS from './apis/ios/latest-transaction-ios';
+import APIsShowManageSubscriptionsIOS from './apis/ios/show-manage-subscriptions-ios';
+import APIsIsTransactionVerifiedIOS from './apis/ios/is-transaction-verified-ios';
+import APIsGetTransactionJwsIOS from './apis/ios/get-transaction-jws-ios';
+import APIsGetReceiptDataIOS from './apis/ios/get-receipt-data-ios';
+import APIsBeginRefundRequestIOS from './apis/ios/begin-refund-request-ios';
+import APIsPresentCodeRedemptionSheetIOS from './apis/ios/present-code-redemption-sheet-ios';
+import APIsGetAppTransactionIOS from './apis/ios/get-app-transaction-ios';
+import APIsCanPresentExternalPurchaseNoticeIOS from './apis/ios/can-present-external-purchase-notice-ios';
+import APIsPresentExternalPurchaseNoticeSheetIOS from './apis/ios/present-external-purchase-notice-sheet-ios';
+import APIsPresentExternalPurchaseLinkIOS from './apis/ios/present-external-purchase-link-ios';
+import APIsIsEligibleForExternalPurchaseCustomLinkIOS from './apis/ios/is-eligible-for-external-purchase-custom-link-ios';
+import APIsGetExternalPurchaseCustomLinkTokenIOS from './apis/ios/get-external-purchase-custom-link-token-ios';
+import APIsShowExternalPurchaseCustomLinkNoticeIOS from './apis/ios/show-external-purchase-custom-link-notice-ios';
+import APIsRequestPurchaseOnPromotedProductIOS from './apis/ios/request-purchase-on-promoted-product-ios';
+import APIsValidateReceiptIOS from './apis/ios/validate-receipt-ios';
+import APIsAcknowledgePurchaseAndroid from './apis/android/acknowledge-purchase-android';
+import APIsConsumePurchaseAndroid from './apis/android/consume-purchase-android';
+import APIsCheckAlternativeBillingAvailabilityAndroid from './apis/android/check-alternative-billing-availability-android';
+import APIsShowAlternativeBillingDialogAndroid from './apis/android/show-alternative-billing-dialog-android';
+import APIsCreateAlternativeBillingTokenAndroid from './apis/android/create-alternative-billing-token-android';
+import APIsEnableBillingProgramAndroid from './apis/android/enable-billing-program-android';
+import APIsIsBillingProgramAvailableAndroid from './apis/android/is-billing-program-available-android';
+import APIsLaunchExternalLinkAndroid from './apis/android/launch-external-link-android';
+import APIsCreateBillingProgramReportingDetailsAndroid from './apis/android/create-billing-program-reporting-details-android';
import Events from './events';
+import EventsPurchaseUpdatedListener from './events/purchase-updated-listener';
+import EventsPurchaseErrorListener from './events/purchase-error-listener';
+import EventsSubscriptionBillingIssueListener from './events/subscription-billing-issue-listener';
+import EventsPromotedProductListenerIOS from './events/ios/promoted-product-listener-ios';
+import EventsUserChoiceBillingListenerAndroid from './events/android/user-choice-billing-listener-android';
+import EventsDeveloperProvidedBillingListenerAndroid from './events/android/developer-provided-billing-listener-android';
import Errors from './errors';
import Purchase from './features/purchase';
import SubscriptionFeature from './features/subscription/index';
@@ -31,11 +99,15 @@ import Discount from './features/discount';
import OfferCodeRedemption from './features/offer-code-redemption';
import ExternalPurchase from './features/external-purchase';
import SubscriptionBillingIssue from './features/subscription-billing-issue';
+import Refund from './features/refund';
+import Validation from './features/validation';
+import Debugging from './features/debugging';
import AlternativeMarketplace from './features/alternative-marketplace/index';
import AlternativeMarketplaceOnside from './features/alternative-marketplace/onside';
import IOSSetup from './ios-setup';
import AndroidSetup from './android-setup';
import HorizonSetup from './horizon-setup';
+import SetupIndex from './setup/index';
import ReactNativeSetup from './setup/react-native';
import ExpoSetup from './setup/expo';
import FlutterSetup from './setup/flutter';
@@ -54,6 +126,16 @@ import FoundationRoadmapBudget from './foundation/roadmap-budget';
import FoundationFoundingSupporters from './foundation/founding-supporters';
import NotFound from '../404';
+/* Preserve the URL hash when redirecting away from a deprecated path so
+ downstream pages (apis/index, types/index) can still translate the
+ anchor into a flat per-symbol page. Without this, a link like
+ /docs/types/request#request-purchase-props would land on /docs/types
+ minus the hash, defeating LEGACY_ANCHOR_REDIRECTS. */
+function NavigatePreservingHash({ to }: { to: string }) {
+ const { hash } = useLocation();
+ return ;
+}
+
function Docs() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
@@ -99,8 +181,16 @@ function Docs() {
- } />
+ }
+ />
+ } />
} />
} />
} />
} />
} />
+ }
+ />
+ } />
} />
- } />
- } />
- } />
- } />
- } />
- } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
} />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
+ } />
+ } />
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
} />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
} />
} />
}
/>
+ } />
+ } />
+ } />
}
@@ -405,6 +1155,7 @@ function Docs() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/packages/docs/src/pages/docs/ios-setup.tsx b/packages/docs/src/pages/docs/ios-setup.tsx
index c8b363d0f..04fbec16a 100644
--- a/packages/docs/src/pages/docs/ios-setup.tsx
+++ b/packages/docs/src/pages/docs/ios-setup.tsx
@@ -1,3 +1,4 @@
+import { Link } from 'react-router-dom';
import HighlightText from '../../components/HighlightText';
import SEO from '../../components/SEO';
@@ -392,8 +393,11 @@ function IOSSetup() {
All transactions must be explicitly finished using{' '}
- finishTransaction() to remove them from the payment
- queue. Unfinished transactions will be re-delivered on app launch.
+
+ finishTransaction()
+ {' '}
+ to remove them from the payment queue. Unfinished transactions will be
+ re-delivered on app launch.
diff --git a/packages/docs/src/pages/docs/lifecycle/index.tsx b/packages/docs/src/pages/docs/lifecycle/index.tsx
index a57529bb8..67ac82f66 100644
--- a/packages/docs/src/pages/docs/lifecycle/index.tsx
+++ b/packages/docs/src/pages/docs/lifecycle/index.tsx
@@ -39,7 +39,7 @@ function LifeCycle() {
Establish connection to the store service using{' '}
- initConnection. This must
+ initConnection. This must
be done before any other IAP operations.
@@ -48,7 +48,7 @@ function LifeCycle() {
Fetch available products from the store using{' '}
- fetchProducts. Products
+ fetchProducts. Products
must be configured in App Store Connect or Google Play Console.
@@ -57,7 +57,7 @@ function LifeCycle() {
User initiates purchase via{' '}
- requestPurchase. The
+ requestPurchase. The
platform payment UI is displayed.
@@ -123,7 +131,7 @@ function LifeCycle() {
7. Transaction Completion
- Call finishTransaction{' '}
+ Call finishTransaction{' '}
to complete the purchase. Unfinished transactions remain in queue and
may cause issues. Set isConsumable=true only for
consumable products, and false or omit for
@@ -135,7 +143,7 @@ function LifeCycle() {
When IAP is no longer needed, call{' '}
- endConnection to free
+ endConnection to free
resources.
iOS: Rich client-side data via{' '}
-
+
RenewalInfoIOS
, but server validation is still recommended for production apps.
@@ -128,11 +128,11 @@ function Subscription() {
Both platforms: Use{' '}
-
+
getActiveSubscriptions
{' '}
or{' '}
-
+
getAvailablePurchases
{' '}
to verify purchases client-side, and implement server-side
@@ -171,7 +171,7 @@ function Subscription() {
-
+
getActiveSubscriptions
@@ -180,7 +180,7 @@ function Subscription() {
-
+
getAvailablePurchases
@@ -698,8 +698,10 @@ function Subscription() {
Always finish transactions: Unfinished
transactions will keep appearing on app launch. Call{' '}
- finishTransaction() after validation and content
- delivery.
+
+ finishTransaction()
+ {' '}
+ after validation and content delivery.
Android 3-day window: Android purchases must be
@@ -730,7 +732,7 @@ function Subscription() {
iOS provides rich subscription data client-side through
StoreKit 2. The{' '}
-
+
RenewalInfoIOS
{' '}
type contains detailed renewal information that lets you build
@@ -739,13 +741,21 @@ function Subscription() {
- RenewalInfoIOS
+
+ RenewalInfoIOS
+
Fields
- This type is available on PurchaseIOS and{' '}
- ActiveSubscriptionIOS via the{' '}
- renewalInfoIOS property:
+ This type is available on{' '}
+
+ PurchaseIOS
+ {' '}
+ and{' '}
+
+ ActiveSubscriptionIOS
+ {' '}
+ via the renewalInfoIOS property:
gracePeriodExpirationDate: Grace period end
@@ -931,25 +944,25 @@ function Subscription() {
-
+
getActiveSubscriptions
{' '}
- Get active subscriptions with renewal info
-
+
getAvailablePurchases
{' '}
- Get all purchases including expired
-
+
subscriptionStatusIOS
{' '}
- Get detailed subscription status
-
+
RenewalInfoIOS
{' '}
- Type reference
@@ -976,7 +989,7 @@ function Subscription() {
The{' '}
-
+
PurchaseAndroid
{' '}
object provides only basic information:
@@ -1006,7 +1019,7 @@ function Subscription() {
Unlike iOS where{' '}
-
+
RenewalInfoIOS
{' '}
provides rich client-side data, the following information on
@@ -1244,13 +1257,13 @@ function Subscription() {
-
+
getActiveSubscriptions
{' '}
- Get active subscriptions (limited data)
-
+
getAvailablePurchases
{' '}
- Get all purchases including subscriptions
@@ -1300,7 +1313,7 @@ function Subscription() {
-
+
APIs: getActiveSubscriptions
{' '}
- API reference
diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx
index 6e6b4578c..f5e3d5f84 100644
--- a/packages/docs/src/pages/docs/setup/expo.tsx
+++ b/packages/docs/src/pages/docs/setup/expo.tsx
@@ -1,3 +1,4 @@
+import { Link } from 'react-router-dom';
import CodeBlock from '../../../components/CodeBlock';
import SEO from '../../../components/SEO';
@@ -399,7 +400,7 @@ function Store() {
apple: { sku: product.productId },
google: { skus: [product.productId] },
},
- type: 'inapp',
+ type: 'in-app',
})
}
/>
@@ -465,7 +466,10 @@ function Store() {
- Errors are automatically normalized to the ErrorCode{' '}
+ Errors are automatically normalized to the{' '}
+
+ ErrorCode
+ {' '}
enum. Use the provided helper functions:
diff --git a/packages/docs/src/pages/docs/setup/index.tsx b/packages/docs/src/pages/docs/setup/index.tsx
new file mode 100644
index 000000000..218761b12
--- /dev/null
+++ b/packages/docs/src/pages/docs/setup/index.tsx
@@ -0,0 +1,161 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+interface FrameworkRow {
+ to: string;
+ name: string;
+ language: string;
+ description: string;
+}
+
+const FRAMEWORKS: FrameworkRow[] = [
+ {
+ to: '/docs/setup/react-native',
+ name: 'React Native',
+ language: 'TypeScript',
+ description:
+ 'Bare React Native CLI projects (RN 0.79+). Built on Nitro Modules with the `useIAP` hook, error normalization, and full StoreKit 2 / Play Billing 8 coverage.',
+ },
+ {
+ to: '/docs/setup/expo',
+ name: 'Expo',
+ language: 'TypeScript',
+ description:
+ 'Expo SDK projects via Expo Modules. Same API surface as react-native-iap, including the `useIAP` hook, with managed-workflow-friendly install. Recommended for any Expo app.',
+ },
+ {
+ to: '/docs/setup/flutter',
+ name: 'Flutter',
+ language: 'Dart',
+ description:
+ 'Flutter apps via the `flutter_inapp_purchase` package. Generated `types.dart`, sealed-class results, and a Stream-based event API that mirrors the OpenIAP schema.',
+ },
+ {
+ to: '/docs/setup/godot',
+ name: 'Godot',
+ language: 'GDScript',
+ description:
+ 'Godot 4.x via the `godot-iap` plugin (iOS GDExtension + Android AAR). Exposes the same OpenIAP function set so the same purchase flow can ship across mobile + console targets.',
+ },
+ {
+ to: '/docs/setup/kmp',
+ name: 'Kotlin Multiplatform',
+ language: 'Kotlin',
+ description:
+ 'KMP / Compose Multiplatform via the `kmp-iap` library. Flow-based API on top of OpenIAP, with CocoaPods integration for iOS targets and shared business logic across platforms.',
+ },
+];
+
+function SetupIndex() {
+ useScrollToHash();
+
+ return (
+
+
+
Framework Setup
+
+ Pick the framework you ship in. Every supported framework wraps the same
+ OpenIAP specification, so the API surface, type names, and event
+ patterns are consistent across stacks — only the install steps differ.
+
+
+
+
+ Supported Frameworks
+
+
+
+
+
Framework
+
Language
+
Description
+
+
+
+ {FRAMEWORKS.map((row) => (
+
+
+
+ {row.name}
+
+
+
+ {row.language}
+
+
{row.description}
+
+ ))}
+
+
+
+
+
+
+ Before You Start
+
+
+ Each framework guide assumes you've already finished the platform
+ store configuration. Complete those first:
+
activeSubscriptions — Populated after{' '}
- getActiveSubscriptions(). Also returns the value
- directly.
+
+ getActiveSubscriptions()
+
+ .
@@ -356,7 +364,10 @@ await endConnection();`}
- Errors are automatically normalized to the ErrorCode{' '}
+ Errors are automatically normalized to the{' '}
+
+ ErrorCode
+ {' '}
enum. Use the provided helper functions:
diff --git a/packages/docs/src/pages/docs/types/active-subscription.tsx b/packages/docs/src/pages/docs/types/active-subscription.tsx
new file mode 100644
index 000000000..327061034
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/active-subscription.tsx
@@ -0,0 +1,335 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import PlatformTabs from '../../../components/PlatformTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function ActiveSubscription() {
+ useScrollToHash();
+
+ return (
+
+
+
ActiveSubscription
+
+
+ ActiveSubscription
+
+
+ Represents an active subscription returned by{' '}
+
+ getActiveSubscriptions()
+
+ . Provides a unified view of subscription status across platforms.
+
+ Deprecated. iOS only — returns null on Android.
+ Use daysUntilExpirationIOS for more precise
+ control.
+
+
+
+
+ transactionId
+
+
+ string
+
+
Transaction identifier for backend validation
+
+
+
+ purchaseToken
+
+
+ string?
+
+
+ JWS token (iOS) or purchase token (Android) for server
+ validation
+
+
+
+
+ transactionDate
+
+
+ number
+
+
Transaction timestamp (epoch ms)
+
+
+
+ currentPlanId
+
+
+ string?
+
+
+ Unified plan identifier. On Android: basePlanId (e.g.,
+ "premium"). On iOS: productId (e.g.,
+ "com.example.premium_monthly"). ⚠️ Android: May
+ be inaccurate for multi-plan subscriptions because Google Play
+ Billing's Purchase object does not expose{' '}
+ basePlanId directly — it has to be inferred. See{' '}
+
+ limitation
+
+ .
+
+ (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+).
+
+ With User Choice Billing (7.0+), users see a dialog to choose between
+ Google Play or your alternative payment. Handle both paths:
+
+
+ {{
+ typescript: (
+ {`import {
+ initConnection,
+ userChoiceBillingListenerAndroid,
+ fetchProducts,
+ requestPurchase,
+} from 'expo-iap';
+
+// Step 1: Set up listener for when user selects alternative billing
+const userChoiceSubscription = userChoiceBillingListenerAndroid(async (details) => {
+ console.log('User chose alternative billing');
+ console.log('Products:', details.products.map(p => p.productId));
+ console.log('External Transaction Token:', details.externalTransactionToken);
+
+ // Process payment with your backend using the token
+ const paymentResult = await yourBackend.processPayment({
+ products: details.products,
+ token: details.externalTransactionToken,
+ });
+
+ if (paymentResult.success) {
+ grantUserAccess();
+ }
+});
+
+// Step 2: Initialize with user choice billing (recommended)
+await initConnection({
+ enableBillingProgramAndroid: 'user-choice-billing',
+});
+
+// Step 3: Fetch products and purchase as normal
+const products = await fetchProducts({
+ request: { 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
+
+// Cleanup
+userChoiceSubscription.remove();`}
+ ),
+ kotlin: (
+ {`import dev.hyo.openiap.store.OpenIapStore
+import dev.hyo.openiap.InitConnectionConfig
+import dev.hyo.openiap.BillingProgramAndroid
+import dev.hyo.openiap.listener.OpenIapUserChoiceBillingListener
+
+val iapStore = OpenIapStore(context)
+
+// Step 1: Set up listener for when user selects alternative billing
+iapStore.addUserChoiceBillingListener(object : OpenIapUserChoiceBillingListener {
+ override fun onUserChoiceBilling(details: UserChoiceBillingDetails) {
+ Log.d("IAP", "User chose alternative billing")
+ Log.d("IAP", "Products: \${details.products.map { it.productId }}")
+ Log.d("IAP", "Token: \${details.externalTransactionToken}")
+
+ // Process payment with your backend using the token
+ lifecycleScope.launch {
+ val paymentResult = yourBackend.processPayment(
+ products = details.products,
+ token = details.externalTransactionToken
+ )
+
+ if (paymentResult.success) {
+ grantUserAccess()
+ }
+ }
+ }
+})
+
+// Step 2: Initialize with user choice billing (recommended)
+iapStore.initConnection(
+ InitConnectionConfig(
+ enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling
+ )
+)
+
+// Step 3: Fetch products and purchase as normal
+val products = iapStore.fetchProducts(
+ 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`}
+ ),
+ dart: (
+ {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
+
+// Step 1: Set up listener for when user selects alternative billing
+final userChoiceSubscription = FlutterInappPurchase.userChoiceBillingStream
+ .listen((details) async {
+ print('User chose alternative billing');
+ print('Products: \${details.products.map((p) => p.productId).toList()}');
+ print('Token: \${details.externalTransactionToken}');
+
+ // Process payment with your backend using the token
+ final paymentResult = await yourBackend.processPayment(
+ products: details.products,
+ token: details.externalTransactionToken,
+ );
+
+ if (paymentResult.success) {
+ grantUserAccess();
+ }
+});
+
+// Step 2: Initialize with user choice billing (recommended)
+await FlutterInappPurchase.instance.initConnection(
+ enableBillingProgramAndroid: BillingProgramAndroid.UserChoiceBilling,
+);
+
+// Step 3: Fetch products and purchase as normal
+final products = await FlutterInappPurchase.instance.getSubscriptions(
+ ['premium_subscription'],
+);
+
+// Step 4: Request purchase - dialog will show both options
+await FlutterInappPurchase.instance.requestSubscription(
+ sku: 'premium_subscription',
+);
+
+// If user selects Google Play → purchaseUpdatedStream fires
+// If user selects alternative → userChoiceBillingStream fires
+
+// Cleanup
+userChoiceSubscription.cancel();`}
+ ),
+ gdscript: (
+ {`# Step 1: Set up listener for when user selects alternative billing
+func _on_user_choice_billing(details: UserChoiceBillingDetails):
+ print("User chose alternative billing")
+ var product_ids = []
+ for p in details.products:
+ product_ids.append(p.product_id)
+ print("Products: %s" % str(product_ids))
+ print("Token: %s" % details.external_transaction_token)
+
+ # 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.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
+await iap.init_connection(config)
+
+# 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 = RequestSubscriptionPropsByPlatforms.new()
+props.request.google = RequestSubscriptionAndroidProps.new()
+props.request.google.skus = ["premium_subscription"]
+props.type = ProductType.SUBS
+await iap.request_purchase(props)
+
+# If user selects Google Play → purchase_updated signal fires
+# If user selects alternative → user_choice_billing signal fires`}
+ ),
+ }}
+
+
+
+ Alternative Billing Only Complete Example
+
+
+ With External Offer mode (replaces Alternative Only), all purchases go
+ through your alternative payment system. Google Play is not shown:
+
+
+ {{
+ typescript: (
+ {`import {
+ initConnection,
+ fetchProducts,
+ checkAlternativeBillingAvailabilityAndroid,
+ showAlternativeBillingDialogAndroid,
+ createAlternativeBillingTokenAndroid,
+} from 'expo-iap';
+
+// Step 1: Initialize with external offer (recommended)
+await initConnection({
+ enableBillingProgramAndroid: 'external-offer',
+});
+
+// Step 2: Check if alternative billing is available
+const isAvailable = await checkAlternativeBillingAvailabilityAndroid();
+if (!isAvailable) {
+ console.log('Alternative billing not available in this region');
+ // Fall back to standard Google Play billing
+ return;
+}
+
+// Step 3: Fetch products (still needed to show prices)
+const products = await fetchProducts({
+ request: { skus: ['premium_subscription'] },
+ type: 'subs',
+});
+
+// Step 4: Show required Google Play disclosure dialog
+const accepted = await showAlternativeBillingDialogAndroid();
+if (!accepted) {
+ console.log('User did not accept alternative billing');
+ return;
+}
+
+// Step 5: Create token for this transaction
+const token = await createAlternativeBillingTokenAndroid(products[0].id);
+
+// Step 6: Process purchase with your backend
+const paymentResult = await yourBackend.processAlternativePurchase({
+ productId: products[0].id,
+ price: products[0].price,
+ token: token,
+ userId: currentUserId,
+});
+
+if (paymentResult.success) {
+ // Report transaction to Google (required)
+ await yourBackend.reportExternalTransaction(token, paymentResult.orderId);
+ grantUserAccess();
+}`}
+ ),
+ kotlin: (
+ {`import dev.hyo.openiap.store.OpenIapStore
+import dev.hyo.openiap.InitConnectionConfig
+import dev.hyo.openiap.BillingProgramAndroid
+
+val iapStore = OpenIapStore(context)
+
+// Step 1: Initialize with external offer (recommended)
+iapStore.initConnection(
+ InitConnectionConfig(
+ enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer
+ )
+)
+
+// Step 2: Check if alternative billing is available
+val availability = iapStore.checkAlternativeBillingAvailability()
+if (!availability.isAvailable) {
+ Log.w("IAP", "Alternative billing not available in this region")
+ // Fall back to standard Google Play billing
+ return
+}
+
+// Step 3: Fetch products (still needed to show prices)
+val products = iapStore.fetchProducts(
+ skus = listOf("premium_subscription"),
+ type = ProductQueryType.Subs
+)
+
+// Step 4: Show required Google Play disclosure dialog
+iapStore.setActivity(activity)
+val dialogResult = iapStore.showAlternativeBillingDialog()
+if (dialogResult.responseCode != 0) {
+ Log.d("IAP", "User did not accept alternative billing")
+ return
+}
+
+// Step 5: Create token for this transaction
+val token = iapStore.createAlternativeBillingToken(products.first().id)
+
+// Step 6: Process purchase with your backend
+lifecycleScope.launch {
+ val paymentResult = yourBackend.processAlternativePurchase(
+ productId = products.first().id,
+ price = products.first().price,
+ token = token,
+ userId = currentUserId
+ )
+
+ if (paymentResult.success) {
+ // Report transaction to Google (required)
+ yourBackend.reportExternalTransaction(token, paymentResult.orderId)
+ grantUserAccess()
+ }
+}`}
+ ),
+ dart: (
+ {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
+
+final iap = FlutterInappPurchase.instance;
+
+// Step 1: Initialize with external offer (recommended)
+await iap.initConnection(
+ enableBillingProgramAndroid: BillingProgramAndroid.ExternalOffer,
+);
+
+// Step 2: Check if alternative billing is available
+final availability = await iap.checkAlternativeBillingAvailability();
+if (!availability.isAvailable) {
+ print('Alternative billing not available in this region');
+ // Fall back to standard Google Play billing
+ return;
+}
+
+// Step 3: Fetch products (still needed to show prices)
+final products = await iap.getSubscriptions(['premium_subscription']);
+
+// Step 4: Show required Google Play disclosure dialog
+final dialogResult = await iap.showAlternativeBillingDialog();
+if (dialogResult.responseCode != 0) {
+ print('User did not accept alternative billing');
+ return;
+}
+
+// Step 5: Create token for this transaction
+final token = await iap.createAlternativeBillingToken(products.first.productId);
+
+// Step 6: Process purchase with your backend
+final paymentResult = await yourBackend.processAlternativePurchase(
+ productId: products.first.productId,
+ price: products.first.price,
+ token: token,
+ userId: currentUserId,
+);
+
+if (paymentResult.success) {
+ // Report transaction to Google (required)
+ await yourBackend.reportExternalTransaction(token, paymentResult.orderId);
+ grantUserAccess();
+}`}
+ ),
+ gdscript: (
+ {`# Step 1: Initialize with external offer (recommended)
+var config = InitConnectionConfig.new()
+config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER
+await iap.init_connection(config)
+
+# Step 2: Check if alternative billing is available
+var availability = await iap.check_alternative_billing_availability()
+if not availability.is_available:
+ print("Alternative billing not available in this region")
+ # Fall back to standard Google Play billing
+ 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: Show required Google Play disclosure dialog
+var dialog_result = await iap.show_alternative_billing_dialog()
+if dialog_result.response_code != 0:
+ print("User did not accept alternative billing")
+ return
+
+# Step 5: Create token for this transaction
+var token = await iap.create_alternative_billing_token(products[0].id)
+
+# Step 6: Process purchase with your backend
+var payment_result = await your_backend.process_alternative_purchase(
+ products[0].id,
+ products[0].price,
+ token,
+ current_user_id
+)
+
+if payment_result.success:
+ # Report transaction to Google (required)
+ await your_backend.report_external_transaction(token, payment_result.order_id)
+ 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.
+
+
+
+
+ );
+}
+
+export default AlternativeBillingTypes;
diff --git a/packages/docs/src/pages/docs/types/alternative.tsx b/packages/docs/src/pages/docs/types/alternative.tsx
deleted file mode 100644
index 9734c5b68..000000000
--- a/packages/docs/src/pages/docs/types/alternative.tsx
+++ /dev/null
@@ -1,1415 +0,0 @@
-import { Link } from 'react-router-dom';
-import AnchorLink from '../../../components/AnchorLink';
-import CodeBlock from '../../../components/CodeBlock';
-import LanguageTabs from '../../../components/LanguageTabs';
-import SEO from '../../../components/SEO';
-import TLDRBox from '../../../components/TLDRBox';
-import { useScrollToHash } from '../../../hooks/useScrollToHash';
-
-function TypesAlternative() {
- useScrollToHash();
-
- return (
-
-
-
Alternative Billing Types
-
- Type definitions for alternative billing systems and external purchase
- links.
-
- Enum controlling which billing system is used during{' '}
- initConnection():
-
-
-
-
-
Name
-
Summary
-
-
-
-
-
- NONE
-
-
Standard Google Play billing (default)
-
-
-
- USER_CHOICE
-
-
- User can select between Google Play or alternative billing
- (requires Billing Library 7.0+)
-
-
-
-
- ALTERNATIVE_ONLY
-
-
- Alternative billing only, no Google Play option (requires
- Billing Library 6.2+)
-
-
-
-
-
-
- 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+).
-
- With User Choice Billing (7.0+), users see a dialog to choose between
- Google Play or your alternative payment. Handle both paths:
-
-
- {{
- typescript: (
- {`import {
- initConnection,
- userChoiceBillingListenerAndroid,
- fetchProducts,
- requestPurchase,
- createAlternativeBillingToken,
-} from 'expo-iap';
-
-// Step 1: Set up listener for when user selects alternative billing
-const userChoiceSubscription = userChoiceBillingListenerAndroid(async (details) => {
- console.log('User chose alternative billing');
- console.log('Products:', details.products.map(p => p.productId));
- console.log('External Transaction Token:', details.externalTransactionToken);
-
- // Process payment with your backend using the token
- const paymentResult = await yourBackend.processPayment({
- products: details.products,
- token: details.externalTransactionToken,
- });
-
- if (paymentResult.success) {
- grantUserAccess();
- }
-});
-
-// Step 2: Initialize with user choice billing (recommended)
-await initConnection({
- enableBillingProgramAndroid: 'user-choice-billing',
-});
-
-// Step 3: Fetch products and purchase as normal
-const products = await fetchProducts({
- request: { 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
-
-// Cleanup
-userChoiceSubscription.remove();`}
- ),
- kotlin: (
- {`import dev.hyo.openiap.store.OpenIapStore
-import dev.hyo.openiap.InitConnectionConfig
-import dev.hyo.openiap.BillingProgramAndroid
-import dev.hyo.openiap.listener.OpenIapUserChoiceBillingListener
-
-val iapStore = OpenIapStore(context)
-
-// Step 1: Set up listener for when user selects alternative billing
-iapStore.addUserChoiceBillingListener(object : OpenIapUserChoiceBillingListener {
- override fun onUserChoiceBilling(details: UserChoiceBillingDetails) {
- Log.d("IAP", "User chose alternative billing")
- Log.d("IAP", "Products: \${details.products.map { it.productId }}")
- Log.d("IAP", "Token: \${details.externalTransactionToken}")
-
- // Process payment with your backend using the token
- lifecycleScope.launch {
- val paymentResult = yourBackend.processPayment(
- products = details.products,
- token = details.externalTransactionToken
- )
-
- if (paymentResult.success) {
- grantUserAccess()
- }
- }
- }
-})
-
-// Step 2: Initialize with user choice billing (recommended)
-iapStore.initConnection(
- InitConnectionConfig(
- enableBillingProgramAndroid = BillingProgramAndroid.UserChoiceBilling
- )
-)
-
-// Step 3: Fetch products and purchase as normal
-val products = iapStore.fetchProducts(
- 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`}
- ),
- dart: (
- {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
-
-// Step 1: Set up listener for when user selects alternative billing
-final userChoiceSubscription = FlutterInappPurchase.userChoiceBillingStream
- .listen((details) async {
- print('User chose alternative billing');
- print('Products: \${details.products.map((p) => p.productId).toList()}');
- print('Token: \${details.externalTransactionToken}');
-
- // Process payment with your backend using the token
- final paymentResult = await yourBackend.processPayment(
- products: details.products,
- token: details.externalTransactionToken,
- );
-
- if (paymentResult.success) {
- grantUserAccess();
- }
-});
-
-// Step 2: Initialize with user choice billing (recommended)
-await FlutterInappPurchase.instance.initConnection(
- enableBillingProgramAndroid: BillingProgramAndroid.UserChoiceBilling,
-);
-
-// Step 3: Fetch products and purchase as normal
-final products = await FlutterInappPurchase.instance.getSubscriptions(
- ['premium_subscription'],
-);
-
-// Step 4: Request purchase - dialog will show both options
-await FlutterInappPurchase.instance.requestSubscription(
- sku: 'premium_subscription',
-);
-
-// If user selects Google Play → purchaseUpdatedStream fires
-// If user selects alternative → userChoiceBillingStream fires
-
-// Cleanup
-userChoiceSubscription.cancel();`}
- ),
- gdscript: (
- {`# Step 1: Set up listener for when user selects alternative billing
-func _on_user_choice_billing(details: UserChoiceBillingDetails):
- print("User chose alternative billing")
- var product_ids = []
- for p in details.products:
- product_ids.append(p.product_id)
- print("Products: %s" % str(product_ids))
- print("Token: %s" % details.external_transaction_token)
-
- # 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.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
-await iap.init_connection(config)
-
-# 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 = RequestSubscriptionPropsByPlatforms.new()
-props.request.google = RequestSubscriptionAndroidProps.new()
-props.request.google.skus = ["premium_subscription"]
-props.type = ProductType.SUBS
-await iap.request_purchase(props)
-
-# If user selects Google Play → purchase_updated signal fires
-# If user selects alternative → user_choice_billing signal fires`}
- ),
- }}
-
-
-
- Alternative Billing Only Complete Example
-
-
- With External Offer mode (replaces Alternative Only), all purchases go
- through your alternative payment system. Google Play is not shown:
-
-
- {{
- typescript: (
- {`import {
- initConnection,
- fetchProducts,
- checkAlternativeBillingAvailability,
- showAlternativeBillingDialog,
- createAlternativeBillingToken,
-} from 'expo-iap';
-
-// Step 1: Initialize with external offer (recommended)
-await initConnection({
- enableBillingProgramAndroid: 'external-offer',
-});
-
-// Step 2: Check if alternative billing is available
-const availability = await checkAlternativeBillingAvailability();
-if (!availability.isAvailable) {
- console.log('Alternative billing not available in this region');
- // Fall back to standard Google Play billing
- return;
-}
-
-// Step 3: Fetch products (still needed to show prices)
-const products = await fetchProducts({
- request: { skus: ['premium_subscription'] },
- type: 'subs',
-});
-
-// Step 4: Show required Google Play disclosure dialog
-const dialogResult = await showAlternativeBillingDialog();
-if (dialogResult.responseCode !== 0) {
- console.log('User did not accept alternative billing');
- return;
-}
-
-// Step 5: Create token for this transaction
-const token = await createAlternativeBillingToken(products[0].id);
-
-// Step 6: Process purchase with your backend
-const paymentResult = await yourBackend.processAlternativePurchase({
- productId: products[0].id,
- price: products[0].price,
- token: token,
- userId: currentUserId,
-});
-
-if (paymentResult.success) {
- // Report transaction to Google (required)
- await yourBackend.reportExternalTransaction(token, paymentResult.orderId);
- grantUserAccess();
-}`}
- ),
- kotlin: (
- {`import dev.hyo.openiap.store.OpenIapStore
-import dev.hyo.openiap.InitConnectionConfig
-import dev.hyo.openiap.BillingProgramAndroid
-
-val iapStore = OpenIapStore(context)
-
-// Step 1: Initialize with external offer (recommended)
-iapStore.initConnection(
- InitConnectionConfig(
- enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer
- )
-)
-
-// Step 2: Check if alternative billing is available
-val availability = iapStore.checkAlternativeBillingAvailability()
-if (!availability.isAvailable) {
- Log.w("IAP", "Alternative billing not available in this region")
- // Fall back to standard Google Play billing
- return
-}
-
-// Step 3: Fetch products (still needed to show prices)
-val products = iapStore.fetchProducts(
- skus = listOf("premium_subscription"),
- type = ProductQueryType.Subs
-)
-
-// Step 4: Show required Google Play disclosure dialog
-iapStore.setActivity(activity)
-val dialogResult = iapStore.showAlternativeBillingDialog()
-if (dialogResult.responseCode != 0) {
- Log.d("IAP", "User did not accept alternative billing")
- return
-}
-
-// Step 5: Create token for this transaction
-val token = iapStore.createAlternativeBillingToken(products.first().id)
-
-// Step 6: Process purchase with your backend
-lifecycleScope.launch {
- val paymentResult = yourBackend.processAlternativePurchase(
- productId = products.first().id,
- price = products.first().price,
- token = token,
- userId = currentUserId
- )
-
- if (paymentResult.success) {
- // Report transaction to Google (required)
- yourBackend.reportExternalTransaction(token, paymentResult.orderId)
- grantUserAccess()
- }
-}`}
- ),
- dart: (
- {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
-
-final iap = FlutterInappPurchase.instance;
-
-// Step 1: Initialize with external offer (recommended)
-await iap.initConnection(
- enableBillingProgramAndroid: BillingProgramAndroid.ExternalOffer,
-);
-
-// Step 2: Check if alternative billing is available
-final availability = await iap.checkAlternativeBillingAvailability();
-if (!availability.isAvailable) {
- print('Alternative billing not available in this region');
- // Fall back to standard Google Play billing
- return;
-}
-
-// Step 3: Fetch products (still needed to show prices)
-final products = await iap.getSubscriptions(['premium_subscription']);
-
-// Step 4: Show required Google Play disclosure dialog
-final dialogResult = await iap.showAlternativeBillingDialog();
-if (dialogResult.responseCode != 0) {
- print('User did not accept alternative billing');
- return;
-}
-
-// Step 5: Create token for this transaction
-final token = await iap.createAlternativeBillingToken(products.first.productId);
-
-// Step 6: Process purchase with your backend
-final paymentResult = await yourBackend.processAlternativePurchase(
- productId: products.first.productId,
- price: products.first.price,
- token: token,
- userId: currentUserId,
-);
-
-if (paymentResult.success) {
- // Report transaction to Google (required)
- await yourBackend.reportExternalTransaction(token, paymentResult.orderId);
- grantUserAccess();
-}`}
- ),
- gdscript: (
- {`# Step 1: Initialize with external offer (recommended)
-var config = InitConnectionConfig.new()
-config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER
-await iap.init_connection(config)
-
-# Step 2: Check if alternative billing is available
-var availability = await iap.check_alternative_billing_availability()
-if not availability.is_available:
- print("Alternative billing not available in this region")
- # Fall back to standard Google Play billing
- 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: Show required Google Play disclosure dialog
-var dialog_result = await iap.show_alternative_billing_dialog()
-if dialog_result.response_code != 0:
- print("User did not accept alternative billing")
- return
-
-# Step 5: Create token for this transaction
-var token = await iap.create_alternative_billing_token(products[0].id)
-
-# Step 6: Process purchase with your backend
-var payment_result = await your_backend.process_alternative_purchase(
- products[0].id,
- products[0].price,
- token,
- current_user_id
-)
-
-if payment_result.success:
- # Report transaction to Google (required)
- await your_backend.report_external_transaction(token, payment_result.order_id)
- 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.
-
-
-
-
-
-
- Billing Programs (Android 8.2.0+)
-
-
- Google Play Billing Library 8.2.0+ introduces the Billing Programs
- API, which provides a more structured approach to external offers and
- content links. Version 8.3.0 adds External Payments for Japan.
-
-
-
- BillingProgramAndroid
-
-
- Enum for different billing program types. Use with{' '}
- enableBillingProgramAndroid in{' '}
- InitConnectionConfig:
-
-
-
-
-
Name
-
Summary
-
Version
-
-
-
-
-
- USER_CHOICE_BILLING
-
-
- User can select between Google Play or alternative billing
-
-
7.0+
-
-
-
- EXTERNAL_CONTENT_LINK
-
-
- For apps that link to external content (reader apps, music
- streaming)
-
-
8.2.0+
-
-
-
- EXTERNAL_OFFER
-
-
- For apps offering alternative payment options (replaces
- ALTERNATIVE_ONLY)
-
-
8.2.0+
-
-
-
- EXTERNAL_PAYMENTS
-
-
- Side-by-side choice between Google Play and developer billing
- (Japan only)
-
-
8.3.0+
-
-
-
-
-
- DeveloperBillingOptionParamsAndroid
-
-
- Parameters for configuring developer billing option in purchase flow
- (8.3.0+):
-
-
-
-
-
Name
-
Type
-
Summary
-
-
-
-
-
- billingProgram
-
-
- BillingProgramAndroid
-
-
- The billing program (usually EXTERNAL_PAYMENTS)
-
-
-
-
- linkUri
-
-
- String
-
-
URL where the external payment will be processed
-
-
-
- launchMode
-
-
- DeveloperBillingLaunchModeAndroid
-
-
How to launch the external payment link
-
-
-
-
-
- DeveloperBillingLaunchModeAndroid
-
-
How the external payment URL is launched:
-
-
-
-
Name
-
Summary
-
-
-
-
-
- LAUNCH_IN_EXTERNAL_BROWSER_OR_APP
-
-
- Google Play launches the link in a browser or eligible app
-
-
-
-
- CALLER_WILL_LAUNCH_LINK
-
-
- Your app handles launching the link after Play returns control
-
-
-
-
-
-
- DeveloperProvidedBillingDetailsAndroid
-
-
Details received when user selects developer billing (8.3.0+):
-
-
-
-
Name
-
Type
-
Summary
-
-
-
-
-
- externalTransactionToken
-
-
- String
-
-
- Token to report external transaction to Google (must report
- within 24 hours)
-
- Token Reporting: When a user completes a purchase
- through developer billing, you must report the{' '}
- externalTransactionToken to Google Play within 24
- hours. See{' '}
-
- External Payments documentation
- {' '}
- for complete implementation details.
-
-
-
-
-
-
- External Purchase Link (iOS)
-
-
- iOS-specific feature for redirecting users to an external website for
- payment using Apple's StoreKit ExternalPurchase API.
- Available from iOS 17.4+ (notice sheet) and iOS 18.2+ (custom links).
-
-
-
-
- Important: External purchase links bypass StoreKit
- completely. No purchaseUpdatedListener will fire. You
- must implement deep links and server-side verification.
-
-
-
-
- External Purchase APIs
-
-
-
-
-
API
-
Description
-
Availability
-
-
-
-
-
- canPresentExternalPurchaseNoticeIOS
-
-
Check if external purchase notice sheet can be presented
- Deprecation Notice: The Android-specific offer types
- (ProductAndroidOneTimePurchaseOfferDetail,{' '}
- ProductSubscriptionAndroidOfferDetails) are deprecated.
- Use the new cross-platform{' '}
- DiscountOffer and SubscriptionOffer{' '}
- types instead.
-
- Important: While Google Play Console allows creating
- multiple base plans for a single subscription product, the{' '}
- basePlanId is not exposed by the Play Billing Library.
- See{' '}
-
- detailed limitation and solutions
-
- .
-
-
- When you have multiple base plans (e.g., monthly and yearly), each
- generates separate SubscriptionOffer objects. Use the{' '}
- offerToken to differentiate between them during purchase.
-
-
-
-
-
Workaround
-
Description
-
-
-
-
-
- Parse billingPeriod
-
-
- Use the billing period (P1M, P1Y) to identify monthly vs yearly
-
-
-
-
Use tags/metadata
-
- Add identifying info in Google Play Console that can be parsed
-
-
-
-
Separate product IDs
-
- Create separate subscription products for each billing period
-
-
-
-
-
-
- );
-}
-
-export default TypesAndroid;
diff --git a/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx b/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx
new file mode 100644
index 000000000..65040eb5e
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/android/one-time-purchase-offer-detail-android.tsx
@@ -0,0 +1,401 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../../components/AnchorLink';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function OneTimePurchaseOfferDetailAndroid() {
+ useScrollToHash();
+
+ return (
+
+ Purchase option ID to identify which option was selected (7.0+)
+
+
+
+
+
+
+ DiscountDisplayInfoAndroid
+
+
+ Discount display metadata Google Play returns for promotional offers
+ (badge text, percentage off). Only populated when the offer is
+ discounted.
+
+
+
+
+
Name
+
Type
+
Summary
+
+
+
+
+
+ discountAmount
+
+
+ string?
+
+
+ Amount discounted from the original price, formatted with
+ currency symbol.
+
+
+
+
+ discountPercentage
+
+
+ string?
+
+
Percentage discount label (e.g. "20%").
+
+
+
+ label
+
+
+ string?
+
+
Display label such as "SALE" or "LIMITED TIME".
+
+
+
+
+
+ ValidTimeWindowAndroid
+
+
Defines the validity period for time-limited offers:
+
+
+
+
Name
+
Type
+
Summary
+
+
+
+
+
+ startTimeMillis
+
+
+ string
+
+
Offer start time (Unix timestamp in milliseconds)
+
+
+
+ endTimeMillis
+
+
+ string
+
+
Offer end time (Unix timestamp in milliseconds)
+
+
+
+
+
+ LimitedQuantityInfoAndroid
+
+
Defines availability for quantity-limited offers:
+
+
+
+
Name
+
Type
+
Summary
+
+
+
+
+
+ maximumQuantity
+
+
+ number
+
+
Maximum number of times offer can be redeemed
+
+
+
+ remainingQuantity
+
+
+ number
+
+
Remaining redemptions available for this user
+
+
+
+
+
+ PreorderDetailsAndroid
+
+
+ Pre-order metadata returned for products that can be pre-purchased
+ (Billing Library 8.1.0+):
+
+
+
+
+
Name
+
Type
+
Summary
+
+
+
+
+
+ releaseTimeMillis
+
+
+ string
+
+
+ Scheduled release timestamp (Unix milliseconds) when the
+ pre-order will be charged.
+
+
+
+
+
+
+ RentalDetailsAndroid
+
+
Rental period metadata for rentable one-time products:
+
+
+
+
Name
+
Type
+
Summary
+
+
+
+
+
+ rentalPeriod
+
+
+ string
+
+
+ ISO 8601 duration the user has access to the rented content
+ (e.g. P30D).
+
+
+
+
+ activationPeriod
+
+
+ string?
+
+
+ Optional ISO 8601 activation grace period during which the
+ rental can be started.
+
+
+
+
+
+
+ );
+}
+
+export default OneTimePurchaseOfferDetailAndroid;
diff --git a/packages/docs/src/pages/docs/types/android/pricing-phase-android.tsx b/packages/docs/src/pages/docs/types/android/pricing-phase-android.tsx
new file mode 100644
index 000000000..094175e4a
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/android/pricing-phase-android.tsx
@@ -0,0 +1,169 @@
+import AnchorLink from '../../../../components/AnchorLink';
+import SEO from '../../../../components/SEO';
+import { useScrollToHash } from '../../../../hooks/useScrollToHash';
+
+function PricingPhaseAndroid() {
+ useScrollToHash();
+
+ return (
+
+ Installment plan details (Play Billing 7.0+, null for
+ non-installment plans)
+
+
+
+
+
+ Note: The offerToken must be passed to{' '}
+
+ requestPurchase()
+ {' '}
+ when purchasing Android subscriptions.
+
+
+
+ InstallmentPlanDetailsAndroid
+
+
+ Installment plan details for subscription offers — Play Billing
+ Library 7.0+. Describes how many committed payments the user signs up
+ for and the subsequent commitment after renewal.
+
+
+
+
+
Name
+
Type
+
Summary
+
+
+
+
+
+ commitmentPaymentsCount
+
+
+ number
+
+
+ Committed payments count after the user signs up. e.g. for a
+ monthly subscription with commitmentPaymentsCount{' '}
+ of 12, the user is billed monthly for 12 months.
+
+
+
+
+ subsequentCommitmentPaymentsCount
+
+
+ number
+
+
+ Committed payments count after the plan renews. Returns{' '}
+ 0 when the installment plan has no subsequent
+ commitment (reverts to a regular plan after the first cycle).
+
+
+
+
+
+
+ );
+}
+
+export default SubscriptionOfferAndroid;
diff --git a/packages/docs/src/pages/docs/types/billing-programs.tsx b/packages/docs/src/pages/docs/types/billing-programs.tsx
new file mode 100644
index 000000000..de6b5f192
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/billing-programs.tsx
@@ -0,0 +1,617 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function BillingPrograms() {
+ useScrollToHash();
+
+ return (
+
+
+
Billing Programs
+
+
+ Billing Programs (Android 8.2.0+)
+
+
+ Google Play Billing Library 8.2.0+ introduces the Billing Programs
+ API, which provides a more structured approach to external offers and
+ content links. Version 8.3.0 adds External Payments for Japan.
+
+ Token Reporting: When a user completes a purchase
+ through developer billing, you must report the{' '}
+ externalTransactionToken to Google Play within 24
+ hours. See{' '}
+
+ External Payments documentation
+ {' '}
+ for complete implementation details.
+
+
+
+
+ );
+}
+
+export default BillingPrograms;
diff --git a/packages/docs/src/pages/docs/types/discount-offer.tsx b/packages/docs/src/pages/docs/types/discount-offer.tsx
new file mode 100644
index 000000000..b7950091b
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/discount-offer.tsx
@@ -0,0 +1,427 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function DiscountOffer() {
+ useScrollToHash();
+
+ return (
+
+
+
DiscountOffer
+
+
+ DiscountOffer
+
+
+ Unified discount-offer type covering both subscription discounts (
+ Introductory, Promotional) and one-time
+ product offers (OneTime, Android only on Google Play
+ Billing Library 7.0+). For iOS-specific WinBack offers see{' '}
+
+ SubscriptionOfferTypeIOS
+
+ ; iOS does not support one-time product discounts.
+
+ Type of offer: Introductory,{' '}
+ Promotional, or OneTime (Android-only
+ Play Billing 7.0+ feature).
+
+
+
+
+
+
+ Android-Specific Fields
+
+
+
+
+
Field
+
Type
+
Description
+
+
+
+
+
+ offerTokenAndroid
+
+
+ String
+
+
+ Required for purchase. Pass to
+ requestPurchase()
+
+
+
+
+ offerTagsAndroid
+
+
+ [String!]
+
+
Tags associated with this offer
+
+
+
+ fullPriceMicrosAndroid
+
+
+ String
+
+
Original price in micro-units (divide by 1,000,000)
+
+
+
+ percentageDiscountAndroid
+
+
+ Int
+
+
Percentage discount (e.g., 33 for 33% off)
+
+
+
+ discountAmountMicrosAndroid
+
+
+ String
+
+
Fixed discount amount in micro-units
+
+
+
+ formattedDiscountAmountAndroid
+
+
+ String
+
+
Formatted discount amount (e.g., "$5.00 OFF")
+
+
+
+ validTimeWindowAndroid
+
+
+
+ ValidTimeWindowAndroid
+
+
+
Time window for limited-time offers
+
+
+
+ limitedQuantityInfoAndroid
+
+
+
+ LimitedQuantityInfoAndroid
+
+
+
Quantity limits for the offer
+
+
+
+ preorderDetailsAndroid
+
+
+
+ PreorderDetailsAndroid
+
+
+
Pre-order details (Billing Library 8.1.0+)
+
+
+
+ rentalDetailsAndroid
+
+
+
+ RentalDetailsAndroid
+
+
+
Rental offer details
+
+
+
+ purchaseOptionIdAndroid
+
+
+ String
+
+
+ Purchase option ID for identifying which purchase option was
+ selected (7.0+)
+
+
+
+
+
+
+ Type Definition
+
+
+ {{
+ typescript: (
+ {`interface DiscountOffer {
+ // Common fields
+ id: string | null;
+ displayPrice: string;
+ price: number;
+ currency: string;
+ type: DiscountOfferType;
+
+ // Android-specific fields
+ offerTokenAndroid?: string;
+ offerTagsAndroid?: string[];
+ fullPriceMicrosAndroid?: string;
+ percentageDiscountAndroid?: number;
+ discountAmountMicrosAndroid?: string;
+ formattedDiscountAmountAndroid?: string;
+ validTimeWindowAndroid?: ValidTimeWindowAndroid;
+ limitedQuantityInfoAndroid?: LimitedQuantityInfoAndroid;
+ preorderDetailsAndroid?: PreorderDetailsAndroid;
+ rentalDetailsAndroid?: RentalDetailsAndroid;
+ purchaseOptionIdAndroid?: string;
+}
+
+enum DiscountOfferType {
+ Introductory = 'Introductory',
+ Promotional = 'Promotional',
+ WinBack = 'WinBack', // iOS 18+
+ OneTime = 'OneTime',
+}`}
+ ),
+ swift: (
+ {`struct DiscountOffer: Codable {
+ // Common fields
+ let id: String?
+ let displayPrice: String
+ let price: Double
+ let currency: String
+ let type: DiscountOfferType
+
+ // Android-specific fields
+ let offerTokenAndroid: String?
+ let offerTagsAndroid: [String]?
+ let fullPriceMicrosAndroid: String?
+ let percentageDiscountAndroid: Int?
+ let discountAmountMicrosAndroid: String?
+ let formattedDiscountAmountAndroid: String?
+ let validTimeWindowAndroid: ValidTimeWindowAndroid?
+ let limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid?
+ let preorderDetailsAndroid: PreorderDetailsAndroid?
+ let rentalDetailsAndroid: RentalDetailsAndroid?
+ let purchaseOptionIdAndroid: String?
+}
+
+enum DiscountOfferType: String, Codable {
+ case introductory = "Introductory"
+ case promotional = "Promotional"
+ case winBack = "WinBack" // iOS 18+
+ case oneTime = "OneTime"
+}`}
+ ),
+ kotlin: (
+ {`data class DiscountOffer(
+ // Common fields
+ val id: String?,
+ val displayPrice: String,
+ val price: Double,
+ val currency: String,
+ val type: DiscountOfferType,
+
+ // Android-specific fields
+ val offerTokenAndroid: String? = null,
+ val offerTagsAndroid: List? = null,
+ val fullPriceMicrosAndroid: String? = null,
+ val percentageDiscountAndroid: Int? = null,
+ val discountAmountMicrosAndroid: String? = null,
+ val formattedDiscountAmountAndroid: String? = null,
+ val validTimeWindowAndroid: ValidTimeWindowAndroid? = null,
+ val limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? = null,
+ val preorderDetailsAndroid: PreorderDetailsAndroid? = null,
+ val rentalDetailsAndroid: RentalDetailsAndroid? = null,
+ val purchaseOptionIdAndroid: String? = null
+)
+
+enum class DiscountOfferType {
+ Introductory,
+ Promotional,
+ WinBack, // iOS 18+
+ OneTime
+}`}
+ ),
+ dart: (
+ {`class DiscountOffer {
+ // Common fields
+ final String? id;
+ final String displayPrice;
+ final double price;
+ final String currency;
+ final DiscountOfferType type;
+
+ // Android-specific fields
+ final String? offerTokenAndroid;
+ final List? offerTagsAndroid;
+ final String? fullPriceMicrosAndroid;
+ final int? percentageDiscountAndroid;
+ final String? discountAmountMicrosAndroid;
+ final String? formattedDiscountAmountAndroid;
+ final ValidTimeWindowAndroid? validTimeWindowAndroid;
+ final LimitedQuantityInfoAndroid? limitedQuantityInfoAndroid;
+ final PreorderDetailsAndroid? preorderDetailsAndroid;
+ final RentalDetailsAndroid? rentalDetailsAndroid;
+ final String? purchaseOptionIdAndroid;
+
+ DiscountOffer({
+ this.id,
+ required this.displayPrice,
+ required this.price,
+ required this.currency,
+ required this.type,
+ this.offerTokenAndroid,
+ this.offerTagsAndroid,
+ this.fullPriceMicrosAndroid,
+ this.percentageDiscountAndroid,
+ this.discountAmountMicrosAndroid,
+ this.formattedDiscountAmountAndroid,
+ this.validTimeWindowAndroid,
+ this.limitedQuantityInfoAndroid,
+ this.preorderDetailsAndroid,
+ this.rentalDetailsAndroid,
+ this.purchaseOptionIdAndroid,
+ });
+}
+
+enum DiscountOfferType {
+ introductory,
+ promotional,
+ winBack, // iOS 18+
+ oneTime,
+}`}
+ ),
+ gdscript: (
+ {`class_name DiscountOffer
+
+# Common fields
+var id: String
+var display_price: String
+var price: float
+var currency: String
+var type: DiscountOfferType
+
+# Android-specific fields
+var offer_token_android: String
+var offer_tags_android: Array[String]
+var full_price_micros_android: String
+var percentage_discount_android: int
+var discount_amount_micros_android: String
+var formatted_discount_amount_android: String
+var valid_time_window_android: ValidTimeWindowAndroid
+var limited_quantity_info_android: LimitedQuantityInfoAndroid
+var preorder_details_android: PreorderDetailsAndroid
+var rental_details_android: RentalDetailsAndroid
+var purchase_option_id_android: String
+
+enum DiscountOfferType {
+ INTRODUCTORY,
+ PROMOTIONAL,
+ WIN_BACK, # iOS 18+
+ ONE_TIME
+}`}
+ ),
+ }}
+
+
+
+ );
+}
+
+export default DiscountOffer;
diff --git a/packages/docs/src/pages/docs/types/external-purchase-link.tsx b/packages/docs/src/pages/docs/types/external-purchase-link.tsx
new file mode 100644
index 000000000..317f3872c
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/external-purchase-link.tsx
@@ -0,0 +1,521 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function ExternalPurchaseLink() {
+ useScrollToHash();
+
+ return (
+
+
+
External Purchase Link Types
+
+
+ External Purchase Link (iOS)
+
+
+ iOS-specific feature for redirecting users to an external website for
+ payment using Apple's StoreKit ExternalPurchase API.
+ Available from iOS 17.4+ (notice sheet) and iOS 18.2+ (custom links).
+
- These types combine platform-specific types with a store{' '}
- discriminator for type-safe handling across Apple, Google, and Horizon
- stores.
-
-
-
- Store Discriminators
-
-
- Each unified type includes a store field that identifies
- the source store:
-
-
-
-
-
Value
-
Summary
-
-
-
-
-
- "apple"
-
-
Apple App Store (iOS/macOS)
-
-
-
- "google"
-
-
Google Play Store (Android)
-
-
-
- "horizon"
-
-
Meta Horizon Store (Quest)
-
-
-
- "unknown"
-
-
Unknown store (default)
-
-
-
-
-
- Note: The platform field is
- deprecated. Use store instead.
-
-
-
-
- Union Types
-
-
The SDK provides these unified types for cross-platform code:
-
-
-
-
Name
-
Summary
-
-
-
-
-
- Product
-
-
- Union of ProductIOS and ProductAndroid
-
-
-
-
- SubscriptionProduct
-
-
- Union of SubscriptionProductIOS and{' '}
- SubscriptionProductAndroid
-
-
-
-
- Purchase
-
-
- Union of PurchaseIOS and{' '}
- PurchaseAndroid
-
-
-
-
-
- Use the platform field to narrow the type and access
- platform-specific fields safely.
-
-
-
-
-
- Storefront
-
-
- Represents the user's App Store or Play Store region, returned by{' '}
- getStorefront().
-
-
-
-
-
Name
-
Summary
-
-
-
-
-
- StorefrontCode
-
-
ISO 3166-1 alpha-2 country code (string)
-
-
-
-
- Example values: "US", "KR",{' '}
- "JP". May return an empty string when the storefront
- cannot be determined.
-
-
-
- iOS sources the value from the active StoreKit storefront. Android
- queries Google Play Billing configuration and returns the same
- country code string when available.
-
-
-
);
}
-export default TypesProduct;
+export default Product;
diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx
index 17074155f..bab1f7533 100644
--- a/packages/docs/src/pages/docs/types/purchase.tsx
+++ b/packages/docs/src/pages/docs/types/purchase.tsx
@@ -1,70 +1,61 @@
+import { Link } from 'react-router-dom';
import AnchorLink from '../../../components/AnchorLink';
-import CodeBlock from '../../../components/CodeBlock';
-import LanguageTabs from '../../../components/LanguageTabs';
import PlatformTabs from '../../../components/PlatformTabs';
import SEO from '../../../components/SEO';
-import TLDRBox from '../../../components/TLDRBox';
import { useScrollToHash } from '../../../hooks/useScrollToHash';
-function TypesPurchase() {
+function Purchase() {
useScrollToHash();
return (
-
Purchase Types
-
- Type definitions for purchase transactions and active subscriptions.
-
-
-
-
-
-
- Purchase
- {' '}
- - Union of PurchaseIOS and PurchaseAndroid
-
Represents a completed or pending purchase transaction. The type is a
- union of PurchaseIOS and PurchaseAndroid,
- discriminated by the platform field.
+ union of{' '}
+
+ PurchaseIOS
+ {' '}
+ and{' '}
+
+ PurchaseAndroid
+
+ , discriminated by the platform field.
+
- Top-level arguments for requestPurchase(). Wraps
- platform-specific props with a type discriminator.
+ Top-level arguments for{' '}
+
+ requestPurchase()
+
+ . Wraps platform-specific props with a type discriminator.
+
Name
+
Type
Summary
- params
+ request
+
+
+
+ RequestPurchasePropsByPlatforms
+
Platform-specific purchase parameters (see below)
@@ -215,7 +84,27 @@ var all_products = await iap.fetch_products(all_request)`}
type
+ Deprecated. Use{' '}
+
+ enableBillingProgramAndroid
+ {' '}
+ in{' '}
+
+ InitConnectionConfig
+ {' '}
+ instead. This flag only logs debug info and has no effect.
- iOS subscriptions use the same props as regular purchases
- (RequestPurchaseIosProps).
+ iOS subscriptions extend{' '}
+
+ RequestPurchaseIosProps
+ {' '}
+ with these additional subscription-only fields:
>
@@ -659,4 +607,4 @@ await iap.request_purchase(subs_props)`}
);
}
-export default TypesRequest;
+export default RequestPurchaseProps;
diff --git a/packages/docs/src/pages/docs/types/storefront.tsx b/packages/docs/src/pages/docs/types/storefront.tsx
new file mode 100644
index 000000000..2b4c2332b
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/storefront.tsx
@@ -0,0 +1,86 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function Storefront() {
+ useScrollToHash();
+
+ return (
+
+
+
Storefront
+
+
+ Storefront
+
+
+ Note:Storefront is not a struct in the
+ OpenIAP GraphQL schema. The schema defines{' '}
+ getStorefront: String!, so the value returned is a plain
+ ISO 3166-1 alpha-2 country-code string. This page exists as a
+ conceptual reference for the value returned by{' '}
+
+ getStorefront()
+
+ .
+
+ ISO 3166-1 alpha-2 country code (e.g. "US",{' '}
+ "KR", "JP"). Empty string when the
+ storefront cannot be determined.
+
+
+
+
+
+
+
+ iOS sources the value from the active StoreKit storefront. Android
+ queries Google Play Billing configuration and returns the same
+ country code string when available.
+
+
+
+
+ );
+}
+
+export default Storefront;
diff --git a/packages/docs/src/pages/docs/types/subscription-offer.tsx b/packages/docs/src/pages/docs/types/subscription-offer.tsx
new file mode 100644
index 000000000..7aa690e18
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/subscription-offer.tsx
@@ -0,0 +1,557 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import CodeBlock from '../../../components/CodeBlock';
+import LanguageTabs from '../../../components/LanguageTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function SubscriptionOffer() {
+ useScrollToHash();
+
+ return (
+
+
+
SubscriptionOffer
+
+
+ SubscriptionOffer
+
+
+ Standardized type for subscription promotional offers. Supported on
+ both iOS (introductory and promotional offers) and Android (offer
+ tokens with pricing phases).
+
+ Required for purchase. Pass to
+ requestPurchase()
+
+
+
+
+ offerTagsAndroid
+
+
+ [String!]
+
+
Tags associated with this offer
+
+
+
+ pricingPhasesAndroid
+
+
+
+ PricingPhasesAndroid
+
+
+
Pricing phases (trial, intro, regular)
+
+
+
+ installmentPlanDetailsAndroid
+
+
+
+ InstallmentPlanDetailsAndroid
+
+
+
+ Installment plan details for subscription commitments (7.0+)
+
+
+
+
+
+
+ Type Definition
+
+
+ {{
+ typescript: (
+ {`interface SubscriptionOffer {
+ // Common fields
+ id: string;
+ displayPrice: string;
+ price: number;
+ currency?: string;
+ type: DiscountOfferType;
+ period?: SubscriptionPeriod;
+ periodCount?: number;
+ paymentMode?: PaymentMode;
+
+ // iOS-specific fields
+ keyIdentifierIOS?: string;
+ nonceIOS?: string;
+ signatureIOS?: string;
+ timestampIOS?: number;
+ numberOfPeriodsIOS?: number;
+ localizedPriceIOS?: string;
+
+ // Android-specific fields
+ basePlanIdAndroid?: string;
+ offerTokenAndroid?: string;
+ offerTagsAndroid?: string[];
+ pricingPhasesAndroid?: PricingPhasesAndroid;
+ installmentPlanDetailsAndroid?: InstallmentPlanDetailsAndroid;
+}
+
+interface InstallmentPlanDetailsAndroid {
+ commitmentPaymentsCount: number;
+ subsequentCommitmentPaymentsCount: number;
+}
+
+interface SubscriptionPeriod {
+ unit: SubscriptionPeriodUnit;
+ value: number;
+}
+
+enum SubscriptionPeriodUnit {
+ Day = 'Day',
+ Week = 'Week',
+ Month = 'Month',
+ Year = 'Year',
+ Unknown = 'Unknown',
+}
+
+enum PaymentMode {
+ FreeTrial = 'FreeTrial',
+ PayAsYouGo = 'PayAsYouGo',
+ PayUpFront = 'PayUpFront',
+ Unknown = 'Unknown',
+}`}
+ ),
+ swift: (
+ {`struct SubscriptionOffer: Codable {
+ // Common fields
+ let id: String
+ let displayPrice: String
+ let price: Double
+ let currency: String?
+ let type: DiscountOfferType
+ let period: SubscriptionPeriod?
+ let periodCount: Int?
+ let paymentMode: PaymentMode?
+
+ // iOS-specific fields
+ let keyIdentifierIOS: String?
+ let nonceIOS: String?
+ let signatureIOS: String?
+ let timestampIOS: Double?
+ let numberOfPeriodsIOS: Int?
+ let localizedPriceIOS: String?
+
+ // Android-specific fields
+ let basePlanIdAndroid: String?
+ let offerTokenAndroid: String?
+ let offerTagsAndroid: [String]?
+ let pricingPhasesAndroid: PricingPhasesAndroid?
+ let installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid?
+}
+
+struct InstallmentPlanDetailsAndroid: Codable {
+ let commitmentPaymentsCount: Int
+ let subsequentCommitmentPaymentsCount: Int
+}
+
+struct SubscriptionPeriod: Codable {
+ let unit: SubscriptionPeriodUnit
+ let value: Int
+}
+
+enum SubscriptionPeriodUnit: String, Codable {
+ case day = "Day"
+ case week = "Week"
+ case month = "Month"
+ case year = "Year"
+ case unknown = "Unknown"
+}
+
+enum PaymentMode: String, Codable {
+ case freeTrial = "FreeTrial"
+ case payAsYouGo = "PayAsYouGo"
+ case payUpFront = "PayUpFront"
+ case unknown = "Unknown"
+}`}
+ ),
+ kotlin: (
+ {`data class SubscriptionOffer(
+ // Common fields
+ val id: String,
+ val displayPrice: String,
+ val price: Double,
+ val currency: String? = null,
+ val type: DiscountOfferType,
+ val period: SubscriptionPeriod? = null,
+ val periodCount: Int? = null,
+ val paymentMode: PaymentMode? = null,
+
+ // iOS-specific fields
+ val keyIdentifierIOS: String? = null,
+ val nonceIOS: String? = null,
+ val signatureIOS: String? = null,
+ val timestampIOS: Double? = null,
+ val numberOfPeriodsIOS: Int? = null,
+ val localizedPriceIOS: String? = null,
+
+ // Android-specific fields
+ val basePlanIdAndroid: String? = null,
+ val offerTokenAndroid: String? = null,
+ val offerTagsAndroid: List? = null,
+ val pricingPhasesAndroid: PricingPhasesAndroid? = null,
+ val installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = null
+)
+
+data class InstallmentPlanDetailsAndroid(
+ val commitmentPaymentsCount: Int,
+ val subsequentCommitmentPaymentsCount: Int
+)
+
+data class SubscriptionPeriod(
+ val unit: SubscriptionPeriodUnit,
+ val value: Int
+)
+
+enum class SubscriptionPeriodUnit {
+ Day, Week, Month, Year, Unknown
+}
+
+enum class PaymentMode {
+ FreeTrial, PayAsYouGo, PayUpFront, Unknown
+}`}
+ ),
+ dart: (
+ {`class SubscriptionOffer {
+ // Common fields
+ final String id;
+ final String displayPrice;
+ final double price;
+ final String? currency;
+ final DiscountOfferType type;
+ final SubscriptionPeriod? period;
+ final int? periodCount;
+ final PaymentMode? paymentMode;
+
+ // iOS-specific fields
+ final String? keyIdentifierIOS;
+ final String? nonceIOS;
+ final String? signatureIOS;
+ final double? timestampIOS;
+ final int? numberOfPeriodsIOS;
+ final String? localizedPriceIOS;
+
+ // Android-specific fields
+ final String? basePlanIdAndroid;
+ final String? offerTokenAndroid;
+ final List? offerTagsAndroid;
+ final PricingPhasesAndroid? pricingPhasesAndroid;
+ final InstallmentPlanDetailsAndroid? installmentPlanDetailsAndroid;
+
+ SubscriptionOffer({
+ required this.id,
+ required this.displayPrice,
+ required this.price,
+ this.currency,
+ required this.type,
+ this.period,
+ this.periodCount,
+ this.paymentMode,
+ this.keyIdentifierIOS,
+ this.nonceIOS,
+ this.signatureIOS,
+ this.timestampIOS,
+ this.numberOfPeriodsIOS,
+ this.localizedPriceIOS,
+ this.basePlanIdAndroid,
+ this.offerTokenAndroid,
+ this.offerTagsAndroid,
+ this.pricingPhasesAndroid,
+ this.installmentPlanDetailsAndroid,
+ });
+}
+
+class InstallmentPlanDetailsAndroid {
+ final int commitmentPaymentsCount;
+ final int subsequentCommitmentPaymentsCount;
+
+ InstallmentPlanDetailsAndroid({
+ required this.commitmentPaymentsCount,
+ required this.subsequentCommitmentPaymentsCount,
+ });
+}
+
+class SubscriptionPeriod {
+ final SubscriptionPeriodUnit unit;
+ final int value;
+
+ SubscriptionPeriod({required this.unit, required this.value});
+}
+
+enum SubscriptionPeriodUnit { day, week, month, year, unknown }
+
+enum PaymentMode { freeTrial, payAsYouGo, payUpFront, unknown }`}
+ ),
+ gdscript: (
+ {`class_name SubscriptionOffer
+
+# Common fields
+var id: String
+var display_price: String
+var price: float
+var currency: String
+var type: DiscountOfferType
+var period: SubscriptionPeriod
+var period_count: int
+var payment_mode: PaymentMode
+
+# iOS-specific fields
+var key_identifier_ios: String
+var nonce_ios: String
+var signature_ios: String
+var timestamp_ios: float
+var number_of_periods_ios: int
+var localized_price_ios: String
+
+# Android-specific fields
+var base_plan_id_android: String
+var offer_token_android: String
+var offer_tags_android: Array[String]
+var pricing_phases_android: PricingPhasesAndroid
+var installment_plan_details_android: InstallmentPlanDetailsAndroid
+
+class InstallmentPlanDetailsAndroid:
+ var commitment_payments_count: int
+ var subsequent_commitment_payments_count: int
+
+class SubscriptionPeriod:
+ var unit: SubscriptionPeriodUnit
+ var value: int
+
+enum SubscriptionPeriodUnit { DAY, WEEK, MONTH, YEAR, UNKNOWN }
+enum PaymentMode { FREE_TRIAL, PAY_AS_YOU_GO, PAY_UP_FRONT, UNKNOWN }`}
+ ),
+ }}
+
+
+
+ );
+}
+
+export default SubscriptionOffer;
diff --git a/packages/docs/src/pages/docs/types/subscription-product.tsx b/packages/docs/src/pages/docs/types/subscription-product.tsx
new file mode 100644
index 000000000..4946a356a
--- /dev/null
+++ b/packages/docs/src/pages/docs/types/subscription-product.tsx
@@ -0,0 +1,283 @@
+import { Link } from 'react-router-dom';
+import AnchorLink from '../../../components/AnchorLink';
+import PlatformTabs from '../../../components/PlatformTabs';
+import SEO from '../../../components/SEO';
+import { useScrollToHash } from '../../../hooks/useScrollToHash';
+
+function SubscriptionProduct() {
+ useScrollToHash();
+
+ return (
+
+
+
ProductSubscription
+
+
+ ProductSubscription
+
+
+ Represents a subscription product available for purchase. Extends the
+ base Product type with subscription-specific fields like pricing
+ phases, introductory offers, and billing periods.
+
New advancedCommerceInfoIOS field on{' '}
- PurchaseIOS — present only for transactions using
- the Advanced Commerce API with generic SKU purchases.
+
+ PurchaseIOS
+ {' '}
+ — present only for transactions using the Advanced Commerce API
+ with generic SKU purchases.
Contains item details, tax info, and refund data from{' '}
@@ -433,8 +437,14 @@ function Releases() {
}}
>
- New getAllTransactionsIOS() query returns the full
- StoreKit 2 transaction history as PurchaseIOS{' '}
+ New{' '}
+
+ getAllTransactionsIOS()
+ {' '}
+ query returns the full StoreKit 2 transaction history as{' '}
+
+ PurchaseIOS
+ {' '}
values.
@@ -2778,7 +2840,10 @@ result.error // optional error`}
Added{' '}
enableBillingProgramAndroid: BillingProgramAndroid{' '}
field for easier billing program setup during{' '}
- initConnection().
+
+ initConnection()
+
+ .
@@ -2794,8 +2859,11 @@ result.error // optional error`}
}}
>
All API methods now automatically call{' '}
- initConnection() internally. No need to manually call
- it before using any API. Backward compatible.
+
+ initConnection()
+ {' '}
+ internally. No need to manually call it before using any API.
+ Backward compatible.