diff --git a/.github/pr-previews/pr-281-setup-readability-expo.jpg b/.github/pr-previews/pr-281-setup-readability-expo.jpg new file mode 100644 index 000000000..a4248775c Binary files /dev/null and b/.github/pr-previews/pr-281-setup-readability-expo.jpg differ diff --git a/.github/pr-previews/pr-281-setup-readability-flutter.jpg b/.github/pr-previews/pr-281-setup-readability-flutter.jpg new file mode 100644 index 000000000..9a59f8949 Binary files /dev/null and b/.github/pr-previews/pr-281-setup-readability-flutter.jpg differ diff --git a/.github/pr-previews/pr-281-setup-readability-react-native.jpg b/.github/pr-previews/pr-281-setup-readability-react-native.jpg new file mode 100644 index 000000000..a1bae2ea9 Binary files /dev/null and b/.github/pr-previews/pr-281-setup-readability-react-native.jpg differ diff --git a/packages/docs/src/components/Callout.tsx b/packages/docs/src/components/Callout.tsx new file mode 100644 index 000000000..4ef2b725a --- /dev/null +++ b/packages/docs/src/components/Callout.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react'; + +type CalloutKind = 'note' | 'tip' | 'important' | 'warning'; + +const KIND_LABELS: Record = { + note: 'Note', + tip: 'Tip', + important: 'Important', + warning: 'Warning', +}; + +interface CalloutProps { + kind?: CalloutKind; + title?: string; + children: ReactNode; +} + +function Callout({ kind = 'note', title, children }: CalloutProps) { + return ( + + ); +} + +export default Callout; diff --git a/packages/docs/src/pages/docs/android-setup.tsx b/packages/docs/src/pages/docs/android-setup.tsx index 592ac15fc..264e51591 100644 --- a/packages/docs/src/pages/docs/android-setup.tsx +++ b/packages/docs/src/pages/docs/android-setup.tsx @@ -1,3 +1,4 @@ +import Callout from '../../components/Callout'; import SEO from '../../components/SEO'; import { GOOGLE_PLAY_BILLING, OPENIAP_VERSIONS } from '../../lib/versioning'; @@ -13,25 +14,12 @@ function AndroidSetup() {

Android Setup Guide

Setting up in-app purchases for Android requires configuration in Google - Play Console and your Android project. + Play Console and your Android project. Building for another + Android-compatible store? See{' '} + Store Setup for Horizon OS, Fire OS, + Vega OS, and alternative marketplace targets.

-
- 📱 Building for another Android-compatible store? See{' '} - - Store Setup - - for Horizon OS, Fire OS, Vega OS, and alternative marketplace targets. -
-

Prerequisites @@ -76,19 +64,11 @@ function AndroidSetup() { -
- âš ī¸ Important: The merchant account must be fully - verified before products become available. This verification can take - 24-48 hours after submitting your information. -
+ + The merchant account must be fully verified before products become + available. This verification can take 24-48 hours after submitting + your information. +

@@ -323,19 +303,11 @@ dependencies {
  • Test cards are automatically used - no real charges occur
  • -
    - 💡 Tip: For faster testing during development, use - Android Debug Bridge (ADB) to clear Google Play Store cache:{' '} + + For faster testing during development, use Android Debug Bridge (ADB) + to clear Google Play Store cache:{' '} adb shell pm clear com.android.vending -
    +
    @@ -378,22 +350,9 @@ dependencies {

    These libraries implement the OpenIAP specification and handle - Android-specific requirements. + Android-specific requirements — refer to each library's + documentation for implementation details.

    - -
    - 💡 Note: Refer to the specific library documentation - for implementation details. Each library follows the OpenIAP - specification while handling platform-specific requirements. -
    @@ -412,7 +371,14 @@ dependencies {

    Android requires acknowledging purchases within 3 days. Unacknowledged - purchases are automatically refunded by Google Play. + purchases are automatically refunded by Google Play. In OpenIAP SDKs + the completion order is: verify with a trusted verifier (your backend + or IAPKit), grant and persist the + entitlement so it survives a restart, then acknowledge through{' '} + + finishTransaction + + .

    @@ -424,6 +390,11 @@ dependencies {

    Consumable products must be consumed before they can be purchased again. This prevents duplicate purchases of items like coins or lives. + Consumption is the{' '} + + finishTransaction + {' '} + call with isConsumable: true.

    @@ -434,8 +405,10 @@ dependencies {

    Always verify purchases through a trusted verifier that calls the - Google Play Developer API, either your backend or IAPKit, to prevent - fraud and ensure purchase validity. + Google Play Developer API — either your backend or{' '} + IAPKit — to prevent fraud and ensure + purchase validity. See the{' '} + Purchase Verification guide.

    @@ -516,20 +489,11 @@ dependencies {
  • Renewal notifications
  • -
    - âš ī¸ Important: OpenIAP libraries handle these - Android-specific requirements automatically. Consult the library - documentation for your chosen framework to ensure proper - implementation. -
    +

    + OpenIAP libraries handle these Android-specific requirements + automatically. Consult the library documentation for your chosen + framework to ensure proper implementation. +

    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 index 496acf792..2b0ae1215 100644 --- a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import SEO from '../../../../components/SEO'; @@ -41,18 +42,16 @@ function AcknowledgePurchaseAndroid() { .

    -
    -

    - âš ī¸ 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. -

    -
    + + 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. +

    Signature

    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 index 99823d035..ec222eda9 100644 --- a/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx +++ b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import SEO from '../../../../components/SEO'; @@ -38,18 +39,15 @@ function ConsumePurchaseAndroid() { .

    -
    -

    - âš ī¸ 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. -

    -
    + + 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. +

    Signature

    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 index ad0b84376..7d799a8e5 100644 --- 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 @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import SEO from '../../../../components/SEO'; @@ -47,20 +48,17 @@ function CreateBillingProgramReportingDetailsAndroid() { .

    -
    -

    - External Offer ordering: check availability, create - fresh reporting details immediately before the redirect, then call{' '} - - launchExternalLinkAndroid() - - . After checkout, send that invocation's token to your backend - and report the transaction to Google within 24 hours. Never reuse a - token from an earlier launch attempt. See the complete{' '} - External Offer flow - . -

    -
    + + Check availability, create fresh reporting details immediately before + the redirect, then call{' '} + + launchExternalLinkAndroid() + + . After checkout, send that invocation's token to your backend and + report the transaction to Google within 24 hours. Never reuse a token + from an earlier launch attempt. See the complete{' '} + External Offer flow. +

    Signature

    diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx index c6c01e08e..b8a1ba5ab 100644 --- a/packages/docs/src/pages/docs/apis/fetch-products.tsx +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import LanguageTabs from '../../../components/LanguageTabs'; import SEO from '../../../components/SEO'; @@ -53,9 +54,8 @@ function FetchProducts() { 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{' '} @@ -87,7 +87,7 @@ function FetchProducts() { .

    -
    +

    Signature

    diff --git a/packages/docs/src/pages/docs/apis/finish-transaction.tsx b/packages/docs/src/pages/docs/apis/finish-transaction.tsx index 10d01000d..c58a60e25 100644 --- a/packages/docs/src/pages/docs/apis/finish-transaction.tsx +++ b/packages/docs/src/pages/docs/apis/finish-transaction.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import LanguageTabs from '../../../components/LanguageTabs'; import SEO from '../../../components/SEO'; @@ -208,13 +209,11 @@ await ((MutationResolver)OpenIapClient.Instance).FinishTransactionAsync( }} -
    -

    - 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. -

    -
    + + 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. + ); } diff --git a/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx index ebd7ec065..48dcebb3b 100644 --- a/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx +++ b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import SEO from '../../../../components/SEO'; @@ -37,13 +38,11 @@ function PresentExternalPurchaseLinkIOS() { .

    -
    -

    - macOS: Not supported. The current OpenIAP Apple core - implementation uses UIApplication and returns a - feature-not-supported error on macOS. -

    -
    + + Not supported. The current OpenIAP Apple core implementation uses{' '} + UIApplication and returns a feature-not-supported error on + macOS. +

    Signature

    diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx index 7dcd1f1aa..838da059c 100644 --- a/packages/docs/src/pages/docs/apis/request-purchase.tsx +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import LanguageTabs from '../../../components/LanguageTabs'; import SEO from '../../../components/SEO'; @@ -46,10 +47,10 @@ function RequestPurchase() { .

    -
    +

    - âš ī¸ Important: APIs starting with request{' '} - are event-based operations, not promise-based. + APIs starting with request are event-based operations, + not promise-based.

    While these APIs return values for various purposes, you should{' '} @@ -82,7 +83,7 @@ function RequestPurchase() { The request prefix indicates that these are event requests — use the appropriate listeners to handle the actual results.

    -
    +

    Signature

    @@ -423,20 +424,18 @@ await ((MutationResolver)OpenIapClient.Instance).RequestPurchaseAsync(new Reques }} -
    -

    - Important: requestPurchase is event-based, not - promise-based. Listen for the result via{' '} - - purchaseUpdatedListener - {' '} - /{' '} - - purchaseErrorListener - - . -

    -
    + + requestPurchase is event-based, not promise-based. Listen for the result + via{' '} + + purchaseUpdatedListener + {' '} + /{' '} + + purchaseErrorListener + + . +

    See:{' '} diff --git a/packages/docs/src/pages/docs/ecosystem.tsx b/packages/docs/src/pages/docs/ecosystem.tsx index 74a788829..61e5f56f5 100644 --- a/packages/docs/src/pages/docs/ecosystem.tsx +++ b/packages/docs/src/pages/docs/ecosystem.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import Callout from '../../components/Callout'; import SEO from '../../components/SEO'; import { useScrollToHash } from '../../hooks/useScrollToHash'; @@ -199,22 +200,12 @@ function Ecosystem() {

    -
    -

    - Maintaining open source libraries requires significant time and - effort. If you find OpenIAP helpful, please consider{' '} - sponsoring to help us sustain and grow this - ecosystem. -

    -
    + + Maintaining open source libraries requires significant time and effort. + If you find OpenIAP helpful, please consider{' '} + sponsoring to help us sustain and grow this + ecosystem. + ); } diff --git a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx index 16d25b90b..e04645c4e 100644 --- a/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx +++ b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import SEO from '../../../../components/SEO'; @@ -237,16 +238,13 @@ using var subscription = iap.PromotedProductIOS.Subscribe(async productId => {' '} on app launch for pending promoted products.

    -
    -

    - Note: In StoreKit 2, promoted products are purchased - through the standard{' '} - - requestPurchase() - {' '} - flow after the app receives or restores the promoted product. -

    -
    + + In StoreKit 2, promoted products are purchased through the standard{' '} + + requestPurchase() + {' '} + flow after the app receives or restores the promoted product. + ); } diff --git a/packages/docs/src/pages/docs/features/debugging.tsx b/packages/docs/src/pages/docs/features/debugging.tsx index f656f242b..2e78249db 100644 --- a/packages/docs/src/pages/docs/features/debugging.tsx +++ b/packages/docs/src/pages/docs/features/debugging.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import IapKitBanner from '../../../components/IapKitBanner'; import PlatformTabs from '../../../components/PlatformTabs'; @@ -83,14 +84,11 @@ OpenIapLog.enable(false)`} Android basePlanId Limitation -
    -

    - Critical Limitation: On Android, the{' '} - currentPlanId and basePlanIdAndroid fields - may return incorrect values for subscription groups with multiple - base plans. -

    -
    + + On Android, the currentPlanId and{' '} + basePlanIdAndroid fields may return incorrect values for + subscription groups with multiple base plans. +

    Root Cause

    @@ -108,15 +106,12 @@ OpenIapLog.enable(false)`} object.

    -
    -

    - Warning log you may see: -

    + Multiple offers (3) found for premium_subscription, using first basePlanId (may be inaccurate) -
    +

    What Works Correctly

    -
    -

    - Critical: If you don't acknowledge an - Android purchase within 3 days, Google will auto-refund the - user. See Purchase{' '} - for the acknowledgment flow. -

    -
    + + 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 diff --git a/packages/docs/src/pages/docs/features/subscription/index.tsx b/packages/docs/src/pages/docs/features/subscription/index.tsx index 1f3885e69..d153240d8 100644 --- a/packages/docs/src/pages/docs/features/subscription/index.tsx +++ b/packages/docs/src/pages/docs/features/subscription/index.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import PlatformTabs from '../../../../components/PlatformTabs'; @@ -55,26 +56,22 @@ function Subscription() { -
    -

    - Availability: billing plan selection requires iOS, - iPadOS, macOS, tvOS, or visionOS 26.4+ and an app compiled with the - StoreKit billing-plan APIs available in Xcode 26.5+ / Swift 6.3+. If - the app runs on an older Apple OS version, omit{' '} - billingPlanType and let StoreKit purchase the default - plan. Never send unknown as a purchase option. -

    -
    - -
    -

    - Treat pricingTermsIOS as the source of truth before - showing a commitment option. If StoreKit does not return a term for - the user's selected billingPlanType, fall back to - the default subscription purchase and leave{' '} - billingPlanType unset. -

    -
    + + Billing plan selection requires iOS, iPadOS, macOS, tvOS, or visionOS + 26.4+ and an app compiled with the StoreKit billing-plan APIs + available in Xcode 26.5+ / Swift 6.3+. If the app runs on an older + Apple OS version, omit billingPlanType and let StoreKit + purchase the default plan. Never send unknown as a + purchase option. + + + + Treat pricingTermsIOS as the source of truth before + showing a commitment option. If StoreKit does not return a term for + the user's selected billingPlanType, fall back to + the default subscription purchase and leave{' '} + billingPlanType unset. + @@ -317,12 +314,10 @@ await iap.request_purchase(props)`}
    -
    -

    - â„šī¸ Tip: Always fetch products first; offers only - exist after {"fetchProducts({ type: 'subs' })"}. -

    -
    + + Always fetch products first; offers only exist after{' '} + {"fetchProducts({ type: 'subs' })"}. +

    Platform Implementation

    @@ -1060,17 +1055,17 @@ async Task PurchaseSubscriptionAsync(string subscriptionId) request's subscriptionOffers entry.

    -
    +

    - âš ī¸ Required: Android subscriptions must - include subscriptionOffers in the purchase - request. Without it, the purchase will fail with: + Android subscriptions must include{' '} + subscriptionOffers in the purchase request. + Without it, the purchase will fail with:

    The number of skus (1) must match: the number of offerTokens (0) -
    + Offer Structure @@ -1680,29 +1675,30 @@ async Task PurchaseSubscriptionAsync(string subscriptionId, ProductSubscriptionA basePlanIdAndroid Limitation -
    -

    - âš ī¸ Google Play Billing API Limitation: The{' '} - - Purchase object - {' '} - returned by Google Play Billing does NOT{' '} - include basePlanIdAndroid. This means{' '} - - getActiveSubscriptions() - {' '} - and purchase callbacks cannot reliably determine which - specific plan was purchased within a subscription group. See{' '} - - detailed limitation and solutions - - . -

    -
    + + The{' '} + + Purchase object + {' '} + returned by Google Play Billing does NOT{' '} + include basePlanIdAndroid. This means{' '} + + getActiveSubscriptions() + {' '} + and purchase callbacks cannot reliably determine which + specific plan was purchased within a subscription group. See{' '} + + detailed limitation and solutions + + . +

    Important distinction: @@ -2109,26 +2105,21 @@ func _on_purchase_success(purchase: PurchaseAndroid) -> void: in your own backend model.

    -
    -

    - 💡 Tip: If you want to simplify server-side - verification, consider using{' '} - - IAPKit - {' '} - which handles all the complexity for you. Use{' '} - - verifyPurchaseWithProvider - {' '} - to verify purchases with minimal setup. -

    -
    + + If you want to simplify server-side verification, consider + using{' '} + + IAPKit + {' '} + which handles all the complexity for you. Use{' '} + + verifyPurchaseWithProvider + {' '} + to verify purchases with minimal setup. + -
    -

    - â„šī¸ Related Resources: -

    -
    + Selecting Specific Offers @@ -2690,12 +2681,9 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void:

    Refund Scenario (Tricky Case)

    -
    -

    - âš ī¸ Important: When a user requests and receives a - refund: -

    -
      + +

      When a user requests and receives a refund:

      +
      1. User purchases subscription
      2. User requests refund from Apple/Google
      3. Refund is approved
      4. @@ -2706,14 +2694,14 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void: may still return the purchase temporarily
      -

      +

      Without server validation: App grants access ✗ (incorrect - refunded!)
      With server validation: Server detects refund → denies access ✓

      -
    + When to Validate @@ -2743,9 +2731,9 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void: -
    +

    - âš ī¸ Android Limitation: While{' '} + While{' '} getAvailablePurchases() {' '} @@ -2753,7 +2741,7 @@ func purchase_with_offer(subscription_id: String, offer_type: int) -> void: time, cancellation status, or refund information. For complete subscription management, consider:

    -
    + Verify Subscription @@ -3571,14 +3559,11 @@ func check_from_active_subscriptions() -> void: android: ( <>

    Android: Limited Client-Side Information

    -
    -

    - âš ī¸ Important: Android's Play Billing - Library does not expose subscription status details (expiry, - cancellation, refund) to the client. You must use - server-side validation. -

    -
    + + Android's Play Billing Library does not expose subscription + status details (expiry, cancellation, refund) to the client. + You must use server-side validation. +

    On Android, client-side checks are limited to verifying if a diff --git a/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx b/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx index 10026d8f1..05794e1cb 100644 --- a/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx +++ b/packages/docs/src/pages/docs/features/subscription/upgrade-downgrade.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; import Accordion from '../../../../components/Accordion'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import PlatformTabs from '../../../../components/PlatformTabs'; @@ -157,29 +158,24 @@ function SubscriptionUpgradeDowngrade() {

    Key insight from Apple Developer Forums:

    -
    - "If the autoRenewPreference value is different - from the productID from{' '} - currentEntitlements, then you know that the - user already changed the subscription plan." -
    -
    — Apple Developer Forums:{' '} -
    - How to know when user upgrades/downgrades - -
    + +
    + "If the autoRenewPreference value is + different from the productID from{' '} + currentEntitlements, then you know that the + user already changed the subscription plan." +
    +

    + — Apple Developer Forums:{' '} + + How to know when user upgrades/downgrades + +

    +
    diff --git a/packages/docs/src/pages/docs/features/validation.tsx b/packages/docs/src/pages/docs/features/validation.tsx index 5fa50e48a..de1f848e1 100644 --- a/packages/docs/src/pages/docs/features/validation.tsx +++ b/packages/docs/src/pages/docs/features/validation.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import IapKitBanner from '../../../components/IapKitBanner'; import LanguageTabs from '../../../components/LanguageTabs'; @@ -56,12 +57,10 @@ function Validation() { -
    -

    - Security: Never rely only on local client purchase - state. Use your backend or IAPKit as the verifier. -

    -
    + + Never rely only on local client purchase state. Use your backend or + IAPKit as the verifier. +
    @@ -500,13 +499,13 @@ if ( "updatedAt": 1784160000000 } }`} -
    +

    - Public, non-authoritative data: The payload is - client-visible and limited to 16 KiB of UTF-8. Never store secrets - or server-only authorization rules in it, and never use its content - instead of isValid, state, or the - store-verified productId for entitlement decisions. + The payload is client-visible and limited to 16 KiB of UTF-8. Never + store secrets or server-only authorization rules in it, and never + use its content instead of isValid, state, + or the store-verified productId for entitlement + decisions.

    A project key compiled into an app can be extracted. It keeps its @@ -514,7 +513,7 @@ if ( project's quota; create separate keys for each build or environment and rotate or revoke a key when a build is retired or compromised.

    -
    +

    This is request/response retrieval, not an APNs or FCM push. To fetch rules when the app opens without verifying a new purchase, use the @@ -544,10 +543,7 @@ if ( Error Handling Best Practice -

    -

    - Important: Verification error ≠ Invalid purchase -

    +

    When verification throws an error, it does NOT mean the purchase is invalid. Errors can occur due to network issues, server downtime, or @@ -561,7 +557,7 @@ if ( At the same time, never grant or finish a new purchase that has not been verified.

    -
    +

    Recommended Pattern

    diff --git a/packages/docs/src/pages/docs/foundation/founding-supporters.tsx b/packages/docs/src/pages/docs/foundation/founding-supporters.tsx index c870bcd18..f654d0e15 100644 --- a/packages/docs/src/pages/docs/foundation/founding-supporters.tsx +++ b/packages/docs/src/pages/docs/foundation/founding-supporters.tsx @@ -1,5 +1,6 @@ import SEO from '../../../components/SEO'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; import { Link } from 'react-router-dom'; @@ -15,21 +16,10 @@ function FoundingSupporters() { keywords="OpenIAP founding supporter, open source sponsor, IAP standard supporter" />

    Become a Founding Supporter

    -
    - Draft — The Foundation section is currently being - prepared. Content may change as the governance structure is finalized. -
    + + The Foundation section is currently being prepared. Content may change + as the governance structure is finalized. +

    We're building OpenIAP into a vendor-neutral, open standard for in-app purchases — and we're looking for organizations to join as Founding diff --git a/packages/docs/src/pages/docs/foundation/governance.tsx b/packages/docs/src/pages/docs/foundation/governance.tsx index fbe206492..87c307464 100644 --- a/packages/docs/src/pages/docs/foundation/governance.tsx +++ b/packages/docs/src/pages/docs/foundation/governance.tsx @@ -1,5 +1,6 @@ import SEO from '../../../components/SEO'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; function Governance() { @@ -14,21 +15,10 @@ function Governance() { keywords="OpenIAP governance, open source governance, TSC, technical steering committee, maintainer policy" />

    Project Governance

    -
    - Draft — The Foundation section is currently being - prepared. Content may change as the governance structure is finalized. -
    + + The Foundation section is currently being prepared. Content may change + as the governance structure is finalized. +

    OpenIAP is an open-source project providing a neutral interoperability standard for in-app purchase APIs and verification across platforms. diff --git a/packages/docs/src/pages/docs/foundation/one-pager.tsx b/packages/docs/src/pages/docs/foundation/one-pager.tsx index 8023c495d..634541ffb 100644 --- a/packages/docs/src/pages/docs/foundation/one-pager.tsx +++ b/packages/docs/src/pages/docs/foundation/one-pager.tsx @@ -1,5 +1,6 @@ import SEO from '../../../components/SEO'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; function OnePager() { @@ -17,21 +18,10 @@ function OnePager() { OpenIAP: Neutral Interoperability Layer for In-App Purchase APIs and Verification -

    - Draft — The Foundation section is currently being - prepared. Content may change as the governance structure is finalized. -
    + + The Foundation section is currently being prepared. Content may change + as the governance structure is finalized. +
    diff --git a/packages/docs/src/pages/docs/foundation/roadmap-budget.tsx b/packages/docs/src/pages/docs/foundation/roadmap-budget.tsx index 4c04ab299..c4a6f7366 100644 --- a/packages/docs/src/pages/docs/foundation/roadmap-budget.tsx +++ b/packages/docs/src/pages/docs/foundation/roadmap-budget.tsx @@ -1,5 +1,6 @@ import SEO from '../../../components/SEO'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; function RoadmapBudget() { @@ -14,21 +15,10 @@ function RoadmapBudget() { keywords="OpenIAP roadmap, funding plan, open source budget, IAP development roadmap" />

    Roadmap & Budget

    -
    - Draft — The Foundation section is currently being - prepared. Content may change as the governance structure is finalized. -
    + + The Foundation section is currently being prepared. Content may change + as the governance structure is finalized. +

    This document outlines how OpenIAP plans to grow and how sponsorship funding is allocated. Full transparency on where every dollar goes. diff --git a/packages/docs/src/pages/docs/foundation/sponsorship.tsx b/packages/docs/src/pages/docs/foundation/sponsorship.tsx index 7e95b8879..c8447a181 100644 --- a/packages/docs/src/pages/docs/foundation/sponsorship.tsx +++ b/packages/docs/src/pages/docs/foundation/sponsorship.tsx @@ -1,5 +1,6 @@ import SEO from '../../../components/SEO'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; import { Link } from 'react-router-dom'; @@ -15,21 +16,10 @@ function Sponsorship() { keywords="OpenIAP sponsorship, open source funding, IAP sponsor, founding supporter" />

    Sponsorship

    -
    - Draft — The Foundation section is currently being - prepared. Content may change as the governance structure is finalized. -
    + + The Foundation section is currently being prepared. Content may change + as the governance structure is finalized. +

    OpenIAP is the open interoperability standard for in-app purchases. Your sponsorship directly funds the infrastructure, security, and diff --git a/packages/docs/src/pages/docs/guides/ai-assistants.tsx b/packages/docs/src/pages/docs/guides/ai-assistants.tsx index 56cf4dccc..e54d8a5bd 100644 --- a/packages/docs/src/pages/docs/guides/ai-assistants.tsx +++ b/packages/docs/src/pages/docs/guides/ai-assistants.tsx @@ -1,4 +1,5 @@ import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; import TLDRBox from '../../../components/TLDRBox'; @@ -283,35 +284,25 @@ getAvailablePurchases in OpenIAP?"`}

    -
    -

    - Tip: For complex implementations, start with{' '} - - llms.txt - {' '} - for quick answers, then reference{' '} - - llms-full.txt - {' '} - when you need detailed type information or platform-specific APIs. -

    -
    + + For complex implementations, start with{' '} + + llms.txt + {' '} + for quick answers, then reference{' '} + + llms-full.txt + {' '} + when you need detailed type information or platform-specific APIs. + ); } diff --git a/packages/docs/src/pages/docs/guides/testing.tsx b/packages/docs/src/pages/docs/guides/testing.tsx index 45efbebd4..325dc73e1 100644 --- a/packages/docs/src/pages/docs/guides/testing.tsx +++ b/packages/docs/src/pages/docs/guides/testing.tsx @@ -1,4 +1,5 @@ import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import SEO from '../../../components/SEO'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; @@ -61,19 +62,10 @@ function Testing() {

    -
    - Warning: Never use your personal Apple ID as a - sandbox account. Always create dedicated sandbox tester accounts in - App Store Connect. -
    + + Never use your personal Apple ID as a sandbox account. Always create + dedicated sandbox tester accounts in App Store Connect. + StoreKit Testing in Xcode @@ -326,20 +318,14 @@ function Testing() { -
    - Warning: Failing to call{' '} - finishTransaction is the most common cause of purchase - issues. Always finish transactions after delivering content, even if - verification fails. -
    + + Failing to call finishTransaction is the most common + cause of purchase issues. Finish every transaction once verification + and delivery succeed — unfinished transactions replay on iOS and are + auto-refunded on Android. If verification fails with a transient + error, leave the transaction unfinished so it is redelivered and can + be retried. + Connection Failed diff --git a/packages/docs/src/pages/docs/ios-setup.tsx b/packages/docs/src/pages/docs/ios-setup.tsx index a1dc34081..6e22483d6 100644 --- a/packages/docs/src/pages/docs/ios-setup.tsx +++ b/packages/docs/src/pages/docs/ios-setup.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; +import Callout from '../../components/Callout'; import HighlightText from '../../components/HighlightText'; import SEO from '../../components/SEO'; @@ -62,20 +63,11 @@ function IOSSetup() { -
    - âš ī¸ Important: These prerequisites are often - overlooked but are absolutely essential. Without completing these - steps, your products will not be found, even if everything else is - configured correctly. -
    + + These prerequisites are often overlooked but are absolutely essential. + Without completing these steps, your products will not be found, even + if everything else is configured correctly. +
    @@ -177,20 +169,11 @@ function IOSSetup() { -
    - 💡 Xcode Version Requirement: Use Xcode 16.4 or - later. Compile with Xcode 27 when you need the StoreKit 27 fields - documented by OpenIAP 3, and complete the UIScene migration below - before shipping that build. -
    +

    + Use Xcode 16.4 or later. Compile with Xcode 27 when you need the + StoreKit 27 fields documented by OpenIAP 3, and complete the UIScene + migration below before shipping that build. +

    1. Adopt UIScene when building with Xcode 27 @@ -344,19 +327,11 @@ function IOSSetup() { -
    - 💡 Note: This is the recommended approach starting - from iOS 15+. The old method of signing into the App Store app with - sandbox credentials is no longer necessary and can cause confusion. -
    + + This is the recommended approach starting from iOS 15+. The old method + of signing into the App Store app with sandbox credentials is no + longer necessary and can cause confusion. +

    @@ -399,22 +374,9 @@ function IOSSetup() {

    These libraries implement the OpenIAP specification and handle - iOS-specific requirements. + iOS-specific requirements — refer to each library's documentation + for implementation details.

    - -
    - 💡 Note: Refer to the specific library documentation - for implementation details. Each library follows the OpenIAP - specification while handling platform-specific requirements. -
    @@ -441,7 +403,11 @@ function IOSSetup() {

    iOS requires purchase verification to validate purchases. StoreKit 2 (iOS 15+) provides JWS (JSON Web Signature) for enhanced security, - while older versions use base64-encoded receipts. + while older versions use base64-encoded receipts. See the{' '} + + Purchase Verification guide + {' '} + for server-side and IAPKit verification flows.

    @@ -468,7 +434,15 @@ function IOSSetup() {

    iOS requires apps to provide a "Restore Purchases" button for users to recover their non-consumable purchases and active subscriptions on new - devices. + devices — wire it to{' '} + + restorePurchases + {' '} + and read results with{' '} + + getAvailablePurchases + + .

    @@ -485,20 +459,11 @@ function IOSSetup() {
  • Family Sharing support (iOS 14+)
  • -
    - âš ī¸ Important: OpenIAP libraries handle these - iOS-specific requirements automatically. Consult the library - documentation for your chosen framework to ensure proper - implementation. -
    +

    + OpenIAP libraries handle these iOS-specific requirements + automatically. Consult the library documentation for your chosen + framework to ensure proper implementation. +

    diff --git a/packages/docs/src/pages/docs/kit-backend.tsx b/packages/docs/src/pages/docs/kit-backend.tsx index 70a8969ae..db03b3744 100644 --- a/packages/docs/src/pages/docs/kit-backend.tsx +++ b/packages/docs/src/pages/docs/kit-backend.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../components/AnchorLink'; +import Callout from '../../components/Callout'; import CodeBlock from '../../components/CodeBlock'; import LanguageTabs from '../../components/LanguageTabs'; import SEO from '../../components/SEO'; @@ -709,7 +710,7 @@ async function refreshEntitlements( }); return snapshot; }`} -
    +

    A 304 still makes one Convex query invocation.{' '} Convex returns a time-independent row snapshot and invalidates its @@ -738,7 +739,7 @@ async function refreshEntitlements( purchase tokens in general-purpose local storage when product IDs, states, and expiry times are sufficient.

    -
    +
    @@ -783,7 +784,7 @@ async function refreshEntitlements( response transfer but still uses a Convex query invocation. -
    +

    Contact us before a high-volume production launch.{' '} If your organization expects to consume a meaningful share of hosted @@ -819,7 +820,7 @@ async function refreshEntitlements( . For capacity planning or a separate written arrangement, contact{' '} hyo@hyo.dev.

    -
    +
    @@ -877,7 +878,7 @@ async function refreshEntitlements( "version": 3, "updatedAt": 1784160000000 }`} -
    +

    Client payloads are public app data. Anyone able to call the project's client endpoints may receive them. Never store @@ -891,7 +892,7 @@ async function refreshEntitlements( independent builds or environments and never put a secret key in the app.

    -
    + Cache a known payload diff --git a/packages/docs/src/pages/docs/lifecycle/index.tsx b/packages/docs/src/pages/docs/lifecycle/index.tsx index 7d4563bb2..80a450438 100644 --- a/packages/docs/src/pages/docs/lifecycle/index.tsx +++ b/packages/docs/src/pages/docs/lifecycle/index.tsx @@ -1,6 +1,7 @@ import { Link } from 'react-router-dom'; import CodeBlock from '../../../components/CodeBlock'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import HighlightText from '../../../components/HighlightText'; import SEO from '../../../components/SEO'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; @@ -109,15 +110,13 @@ function LifeCycle() { -
    -

    - â„šī¸ Tip: Use{' '} - - verifyPurchase - {' '} - for server-side purchase verification. -

    -
    + + Use{' '} + + verifyPurchase + {' '} + for server-side purchase verification. + 6. Content Delivery diff --git a/packages/docs/src/pages/docs/setup/expo.tsx b/packages/docs/src/pages/docs/setup/expo.tsx index a15c71507..4e61e60b2 100644 --- a/packages/docs/src/pages/docs/setup/expo.tsx +++ b/packages/docs/src/pages/docs/setup/expo.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; import { @@ -18,38 +19,19 @@ function ExpoSetup() { />

    Expo Setup

    - expo-iap provides in-app purchase support for Expo apps. - Works with both managed and bare workflows. + expo-iap provides in-app purchase support for Expo apps — + both the managed workflow and bare apps that prefer the Expo Modules + stack. For bare React Native we recommend{' '} + react-native-iap (same API); + see React Native CLI Projects for using expo-iap + in bare apps.

    -
    - Use this if you're using Expo managed workflow. If you're using bare - React Native, use{' '} - react-native-iap instead. -
    - -
    - Before you start: Complete the store configuration - before integrating with your framework:{' '} - iOS Setup |{' '} - Android Setup -
    + + Complete the store configuration before integrating with your framework:{' '} + iOS Setup |{' '} + Android Setup +

    @@ -93,18 +75,9 @@ function ExpoSetup() { -
    - Development build required: In-app purchases require - native modules that are not available in Expo Go. You - must use a{' '} + + In-app purchases require native modules that are{' '} + not available in Expo Go. You must use a{' '} . Testing also requires a physical device — simulators and emulators have limited IAP support. -
    +

    @@ -139,12 +112,15 @@ function ExpoSetup() {

    • - Expo SDK 54+: Use the default toolchain when it is - already Kotlin 2.2 compatible, or set the version explicitly below. + Expo SDK 54+: the default toolchain is often + sufficient, but the OpenIAP artifacts require Kotlin 2.2+ — if the + Android build fails on Kotlin metadata, set{' '} + kotlinVersion explicitly with expo-build-properties as + shown below.
    • - Expo SDK 53: Explicitly set the Kotlin version when - building Android apps: + Expo SDK 53: Set the Kotlin version explicitly with + expo-build-properties:
    @@ -165,21 +141,13 @@ function ExpoSetup() { }`} -
    + + Expo SDK 52 uses Kotlin 1.9.x, which is incompatible{' '} + with Billing Library v8. You must either upgrade to SDK 53+ + (recommended) or use a custom config plugin to downgrade the billing + library. See the SDK 52 workaround{' '} + below. +

    Prebuild & Development Build @@ -237,18 +205,17 @@ export default { + Capability > In-App Purchase{' '} (after running npx expo prebuild).

    -

    - Xcode 27 builds must use the UIScene lifecycle. Regenerate with an - Expo template that creates React Native from{' '} - ExpoAppSceneDelegate, or migrate an older generated iOS - host before building. Confirm that Info.plist contains a - scene configuration and that AppDelegate.swift no longer - creates UIWindow(frame: UIScreen.main.bounds). See the{' '} + + The generated iOS project must use the UIScene lifecycle. Follow the{' '} Xcode 27 UIScene checklist - - . -

    + {' '} + to migrate: your Info.plist needs a scene configuration, + and AppDelegate.swift must no longer create{' '} + UIWindow(frame: UIScreen.main.bounds). Newer Expo + templates (based on ExpoAppSceneDelegate) generate this + correctly. +

    Android Configuration @@ -287,7 +254,17 @@ cd ios && pod install`} #

    -

    The expo-iap config plugin supports these options:

    +

    + The expo-iap config plugin does two things: it wires your IAPKit + publishable key into the app for hosted{' '} + purchase verification, and it + enables optional store modules —{' '} + Onside (an iOS alternative + marketplace), Horizon OS{' '} + (Meta Quest), and Amazon{' '} + (Fire OS devices and the Vega OS runtime). All modules are off by + default; enable only the stores you ship to. +

    {`{ "expo": { @@ -316,32 +293,19 @@ cd ios && pod install`} }`}

    - Use this page for the Expo plugin shape. Store-specific values, + Use this page for the Expo plugin shape. Store-specific values — required developer-console fields, supported targets, and artifact - rules live in Store Setup: + rules — live in each store's setup page linked above.

    -

    - Keep module enable flags under modules and - platform-specific values under android or{' '} - ios. For Amazon targets, use{' '} - modules.amazon.fireOS and{' '} - modules.amazon.vegaOS; use{' '} - android.amazon.vegaOS only when Vega metadata must differ - from the normal Expo app config. + Module enable flags live under modules; platform-specific + values live under android or ios. For + Amazon, modules.amazon.fireOS and{' '} + modules.amazon.vegaOS toggle each target; the separate{' '} + android.amazon.vegaOS block is only needed when your Vega + OS build requires different values (app id, artifacts) than your + regular Android config — see{' '} + Amazon Store Setup.

    @@ -353,6 +317,41 @@ cd ios && pod install`} +

    + Under the hood, the typical flow is{' '} + + initConnection + {' '} + → set up{' '} + + purchaseUpdatedListener + {' '} + and{' '} + + purchaseErrorListener + {' '} + →{' '} + + fetchProducts + {' '} + →{' '} + + requestPurchase + {' '} + →{' '} + + finishTransaction + + , with{' '} + + endConnection + {' '} + on teardown. The useIAP hook manages the connection and + listener steps for you. See the{' '} + Purchase Guide for the + complete flow. +

    +

    useIAP Hook (Recommended) @@ -416,34 +415,37 @@ function Store() { }`} -
    - Critical: Always call finishTransaction{' '} - after verifying a purchase. On Android, unfinished purchases are - automatically refunded after 3 days. -
    - -
    - Important: The useIAP hook API is - identical to react-native-iap. Methods return{' '} - Promise<void> and update internal state. Use{' '} - onPurchaseSuccess for purchase results. -
    +

    + Each call here has a full reference — see{' '} + + fetchProducts + + ,{' '} + + requestPurchase + + , and{' '} + + finishTransaction + {' '} + for parameters and per-store behavior, and{' '} + + ErrorCode + {' '} + for the full error reference. +

    + + + Always call finishTransaction after verifying a purchase. + On Android, unfinished purchases are automatically refunded after 3 + days. + + +

    + The useIAP hook API is identical to react-native-iap: + methods return Promise<void> and update internal + state — use onPurchaseSuccess for purchase results. +

    @@ -453,14 +455,23 @@ function Store() { # +

    + expo-iap and react-native-iap share the same OpenIAP API; they differ + only in tooling: +

    • Uses npx expo install instead of{' '} npm install
    • Supports Expo managed workflow (no manual native code needed)
    • -
    • Uses Expo modules architecture instead of Nitro Modules
    • -
    • Same API surface and hook behavior as react-native-iap
    • +
    • + Built on the Expo Modules architecture instead of{' '} + + Nitro Modules + + , the C++/JSI binding layer react-native-iap uses +
    @@ -503,50 +514,6 @@ switch (error.code) { -
    -

    - Next Steps - - # - -

    - -
    -

    tvOS Support @@ -566,7 +533,12 @@ switch (error.code) { . Requires tvOS 16.0+.

    -

    Configuration

    +

    + Configuration + + # + +

    Replace react-native with react-native-tvos{' '} in your package.json: @@ -620,19 +592,11 @@ EXPO_TV=1 npx expo prebuild --platform ios --clean EXPO_TV=1 npx expo run:ios --device "Apple TV 4K (3rd generation)"`} -

    - Note: presentCodeRedemptionSheetIOS is{' '} + + presentCodeRedemptionSheetIOS is{' '} not supported on tvOS. Direct users to redeem codes on their iPhone or through Apple TV settings instead. -
    +
    @@ -643,20 +607,12 @@ EXPO_TV=1 npx expo run:ios --device "Apple TV 4K (3rd generation)"`} -
    - Warning: Expo SDK 52 (React Native 0.76.x) uses - Kotlin 1.9.x, which is incompatible with the current OpenIAP Android - artifacts. Upgrading to SDK 53+ and setting Kotlin - 2.2.0 is the recommended solution. -
    + + Expo SDK 52 (React Native 0.76.x) uses Kotlin 1.9.x, which is + incompatible with the current OpenIAP Android artifacts — upgrading to{' '} + SDK 53+ is the recommended fix (see{' '} + Android Kotlin Version). +

    If you cannot upgrade, create a custom config plugin to force an older @@ -697,7 +653,12 @@ module.exports = function withBillingLibraryDowngrade(config) { -

    Products not found

    +

    + Products not found + + # + +

    • Ensure all agreements are signed in App Store Connect / Google Play @@ -714,7 +675,12 @@ module.exports = function withBillingLibraryDowngrade(config) {
    • Wait 15-30 minutes after creating products before testing
    -

    Build issues

    +

    + Build issues + + # + +

    • Clear and reinstall:{' '} @@ -734,6 +700,55 @@ module.exports = function withBillingLibraryDowngrade(config) {
    + +
    +

    + Next Steps + + # + +

    +
      +
    • + Purchase Guide — Complete + purchase flow with validation and receipt verification +
    • +
    • + Subscription Guide — + Subscription offers, renewal, and management +
    • +
    • + Error Codes — Full error reference + and handling strategies +
    • +
    • + API Reference — All available APIs with + multi-language examples +
    • +
    • + Store Setup — support boundaries + for Onside, Horizon OS (Meta Quest), and Amazon (Fire OS / Vega OS) + targets +
    • +
    • + + npm: expo-iap + + {' | '} + + GitHub Source + +
    • +
    +
    ); } diff --git a/packages/docs/src/pages/docs/setup/flutter.tsx b/packages/docs/src/pages/docs/setup/flutter.tsx index 216e7e9a7..9e6daad2a 100644 --- a/packages/docs/src/pages/docs/setup/flutter.tsx +++ b/packages/docs/src/pages/docs/setup/flutter.tsx @@ -1,4 +1,5 @@ import { Link } from 'react-router-dom'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; import { ANDROID_SDK, FLUTTER_PACKAGE } from '../../../lib/versioning'; @@ -18,20 +19,41 @@ function FlutterSetup() { Flutter apps on iOS, Android, and macOS.

    -
    - Before you start: Complete the store configuration - before integrating with your framework:{' '} + + Complete the store configuration before integrating with your framework:{' '} iOS Setup |{' '} Android Setup -
    + + +
    +

    + Prerequisites + + # + +

    +
      +
    • + Flutter with iOS 15.0+ /{' '} + macOS 14.0+ / Android{' '} + minSdkVersion {ANDROID_SDK.minSdk}+ (see the platform + sections below) +
    • +
    • + An active Apple Developer account and/or Google Play Developer + account +
    • +
    • + Products configured in the store consoles — see{' '} + iOS Setup and{' '} + Android Setup +
    • +
    • + A real device for purchase testing — simulators and emulators cannot + complete purchases +
    • +
    +

    @@ -106,8 +128,10 @@ function FlutterSetup() {

    - Add the following to your ios/Runner/Info.plist (iOS - 14+): + Declaring itms-apps in ios/Runner/Info.plist{' '} + is only needed when your own code checks App Store links before + opening them (for example canLaunchUrl from url_launcher) + — the plugin itself does not require it:

    {`LSApplicationQueriesSchemes @@ -172,21 +196,16 @@ function FlutterSetup() { }`} -
    - Note: The missingDimensionStrategy{' '} - configuration is required since v7.1.14 due to product flavor support - for Meta Horizon OS and Fire OS. Keep this page focused on Flutter - installation; use Store Setup for target-specific Android flavor - details. -
    + + The missingDimensionStrategy line is required since + v7.1.14 because the Android library ships product flavors for + alternative stores — Meta Horizon OS (Quest headsets) and Amazon Fire + OS. Apps shipping to Google Play always select the play{' '} + flavor shown above. Targeting Meta Quest or Amazon devices instead? + See Horizon Store Setup or{' '} + Amazon Store Setup for the + flavor to select there. +

    ProGuard Rules (if using ProGuard)

    @@ -207,25 +226,35 @@ function FlutterSetup() { # - -

    - Android purchase JSON: the public Purchase field is{' '} - dataAndroid. Flutter 10 does not accept the former - custom-channel alias. Native adapters, MethodChannel fixtures, and - mocks must emit dataAndroid. See{' '} - - Deprecations & 3.0 Migration - - . -
    +

    + The typical flow is{' '} + + initConnection + {' '} + → set up{' '} + + purchaseUpdatedListener + {' '} + and{' '} + + purchaseErrorListener + {' '} + →{' '} + + fetchProducts + {' '} + →{' '} + + requestPurchase + {' '} + →{' '} + + finishTransaction + + . The snippets below cover each step; the{' '} + Purchase Guide shows the full + flow with receipt validation. +

    Basic Setup @@ -235,51 +264,93 @@ function FlutterSetup() {

    {`import 'dart:async'; + +import 'package:flutter/material.dart'; import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; -final iap = FlutterInappPurchase.instance; - -late StreamSubscription purchaseSub; -late StreamSubscription errorSub; - -// Initialize connection -await iap.initConnection(); - -// Setup listeners -purchaseSub = iap.purchaseUpdatedListener.listen((purchase) async { - // Validate receipt, then: - // CRITICAL: Android auto-refunds after 3 days if not called! - await iap.finishTransaction(purchase: purchase, isConsumable: true); -}); - -errorSub = iap.purchaseErrorListener.listen((error) { - if (error.code == ErrorCode.UserCancelled) return; - print('\${error.code}: \${error.message}'); -}); - -// Cleanup in dispose() -@override -void dispose() { - purchaseSub.cancel(); - errorSub.cancel(); - unawaited(iap.endConnection()); - super.dispose(); +class StoreScreen extends StatefulWidget { + const StoreScreen({super.key}); + + @override + State createState() => _StoreScreenState(); +} + +class _StoreScreenState extends State { + final iap = FlutterInappPurchase.instance; + + StreamSubscription? purchaseSub; + StreamSubscription? errorSub; + + @override + void initState() { + super.initState(); + _init(); + } + + Future _init() async { + // Initialize connection + await iap.initConnection(); + + // Setup listeners + purchaseSub = iap.purchaseUpdatedListener.listen((purchase) async { + // Verify the purchase (server-side), then finish it + await iap.finishTransaction( + purchase: purchase, + isConsumable: false, // true for consumables + ); + }); + + errorSub = iap.purchaseErrorListener.listen((error) { + if (error.code == ErrorCode.UserCancelled) return; + print('\${error.code}: \${error.message}'); + }); + } + + @override + void dispose() { + purchaseSub?.cancel(); + errorSub?.cancel(); + unawaited(iap.endConnection()); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return const SizedBox.shrink(); // Your store UI + } }`} +

    + Each call here has a full reference — see{' '} + + initConnection + + ,{' '} + + purchaseUpdatedListener + + ,{' '} + + purchaseErrorListener + + ,{' '} + + finishTransaction + + , and{' '} + + endConnection + {' '} + for parameters and per-store behavior, and{' '} + Error Codes for the ErrorCode{' '} + values. +

    -
    - Critical: Always call finishTransaction{' '} - after verifying a purchase. On Android, unfinished purchases are - automatically refunded after 3 days. -
    + + Always call finishTransaction after verifying a purchase. + On Android, unfinished purchases are automatically refunded after 3 + days. +

    Fetching Products @@ -305,6 +376,13 @@ for (final product in products) { print('\${product.title}: \${product.displayPrice}'); }`} +

    + See{' '} + + fetchProducts + {' '} + for parameters and per-store behavior. +

    Making a Purchase @@ -312,6 +390,19 @@ for (final product in products) { #

    +

    + + requestPurchase + {' '} + does not return the purchase. Results arrive on the{' '} + + purchaseUpdatedListener + {' '} + stream you set up in Basic Setup (unlike + the callback-style hooks in the React Native SDKs), so make sure both + listeners are active before you call it. +

    + {`// Request purchase (results come through purchaseUpdatedListener) await iap.requestPurchase( @@ -333,22 +424,6 @@ await iap.requestPurchase( );`} -
    - Important: Flutter uses Streams for - purchase events, not callbacks. Always set up{' '} - purchaseUpdatedListener and{' '} - purchaseErrorListener listeners before calling{' '} - requestPurchase. -
    -

    Restoring Purchases @@ -364,54 +439,13 @@ final allPurchases = await iap.getAvailablePurchases( onlyIncludeActiveItemsIOS: false, );`} -

    - -
    -

    - Next Steps - - # - -

    - +

    + See{' '} + + getAvailablePurchases + {' '} + for parameters and per-store behavior. +

    @@ -467,6 +501,68 @@ final allPurchases = await iap.getAvailablePurchases( Implement proper handling for PurchaseState.Pending + +

    Purchase JSON missing dataAndroid (Flutter 10)

    +

    + The public Purchase field is{' '} + dataAndroid. Flutter 10 does not accept the former + custom-channel alias. Native adapters, MethodChannel fixtures, and + mocks must emit dataAndroid. See{' '} + + Deprecations & 3.0 Migration + + . +

    +
    + +
    +

    + Next Steps + + # + +

    +
      +
    • + Purchase Guide — Complete + purchase flow with validation and receipt verification +
    • +
    • + Subscription Guide — + Subscription offers, renewal, and management +
    • +
    • + Error Codes — Full error reference and + handling strategies +
    • +
    • + API Reference — All available APIs with + multi-language examples +
    • +
    • + Store Setup — targeting alternative + stores such as Meta Horizon OS (Quest headsets) and Amazon Fire OS, + plus which OpenIAP SDKs support each store (Amazon's Vega OS, + for example, is available only in the React Native and Expo SDKs) +
    • +
    • + + pub.dev: flutter_inapp_purchase + + {' | '} + + GitHub Source + +
    • +
    ); diff --git a/packages/docs/src/pages/docs/setup/godot.tsx b/packages/docs/src/pages/docs/setup/godot.tsx index 6732e05e1..5e4e6cf32 100644 --- a/packages/docs/src/pages/docs/setup/godot.tsx +++ b/packages/docs/src/pages/docs/setup/godot.tsx @@ -1,3 +1,4 @@ +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; @@ -17,20 +18,11 @@ function GodotSetup() { and Kotlin AAR for Android.

    -
    - Before you start: Complete the store configuration - before integrating with your framework:{' '} + + Complete the store configuration before integrating with your framework:{' '} iOS Setup |{' '} Android Setup -
    +

    @@ -89,46 +81,6 @@ function GodotSetup() {

    The zip includes pre-built binaries for both iOS and Android.

    -

    - Native Apple API availability is fixed when the pre-built{' '} - GodotIap.framework is compiled. In particular, the - verified Apple 27 offer-code result requires a framework built with - Xcode 27 or later; a framework built with Xcode 26 still presents the - legacy sheet and returns null, even when the app runs on - Apple 27. The published godot-iap 3.0.0 iOS framework is built with - Xcode 27 and its release workflow rejects an older artifact. Custom - builds must use Xcode 27 to retain that result path. -

    - -

    - macOS: Damaged Framework Warning - - # - -

    -

    - Release zips are intended for iOS export and Android. If you use a - release or custom build that includes{' '} - addons/godot-iap/bin/macos, and Godot reports that{' '} - GodotIap.framework or{' '} - SwiftGodotRuntime.framework is damaged on macOS, clear - quarantine and repair the local ad-hoc signature: -

    - - {`# Run from your Godot project root after copying addons/godot-iap -xattr -dr com.apple.quarantine addons/godot-iap -codesign --force --deep --sign - --timestamp=none addons/godot-iap/bin/macos/SwiftGodotRuntime.framework -codesign --force --deep --sign - --timestamp=none addons/godot-iap/bin/macos/GodotIap.framework`} - -

    - The checked-in macOS runtime frameworks are Apple Silicon ( - arm64) only. Custom source builds can override{' '} - MACOS_ARCHS; make macos requests{' '} - arm64 x86_64 by default, and generated metadata should - only include architectures that the framework binaries actually - contain. The default release zip does not include macOS runtime - frameworks. -

    Build from Source @@ -149,6 +101,56 @@ make android # Copy addons/godot-iap/ to your project`} +

    + The checked-in macOS runtime frameworks are Apple Silicon ( + arm64) only. Custom source builds can override{' '} + MACOS_ARCHS; make macos requests{' '} + arm64 x86_64 by default, and generated metadata should + only include architectures that the framework binaries actually + contain. +

    + +

    + iOS Framework Toolchain (Offer Codes) + + # + +

    +

    + The pre-built iOS framework locks in Apple API availability at compile + time. One feature depends on this:{' '} + + offer code redemption + {' '} + only returns a verified result when the framework was built with Xcode + 27 or later — a framework built with Xcode 26 falls back to the legacy + redemption sheet and returns null, even on devices + running the latest OS. The published godot-iap 3.0.0 framework is + built with Xcode 27. If you build from source and use offer codes, + build with Xcode 27 or later. +

    + +

    + macOS: Damaged Framework Warning + + # + +

    +

    + The default release zip does not include macOS runtime frameworks, so + most projects can skip this section. It applies only if you build from + source with macOS support or use a custom zip containing{' '} + addons/godot-iap/bin/macos. If Godot reports that{' '} + GodotIap.framework or{' '} + SwiftGodotRuntime.framework is damaged, clear quarantine + and repair the ad-hoc signature: +

    + + {`# Run from your Godot project root after copying addons/godot-iap +xattr -dr com.apple.quarantine addons/godot-iap +codesign --force --deep --sign - --timestamp=none addons/godot-iap/bin/macos/SwiftGodotRuntime.framework +codesign --force --deep --sign - --timestamp=none addons/godot-iap/bin/macos/GodotIap.framework`} +
    @@ -266,17 +268,8 @@ make android -
    - Warning: If the frameworks are not embedded, the app - will crash on launch with:{' '} + + If the frameworks are not embedded, the app will crash on launch with:{' '} Library not loaded: @rpath/GodotIap.framework/GodotIap. If the runpath is missing, it can crash with:{' '} @@ -284,7 +277,7 @@ make android @rpath/SwiftGodotRuntime.framework/SwiftGodotRuntime . -
    +

    iOS: Missing Info.plist Fallback @@ -337,76 +330,10 @@ fi`} Frameworks" phase -
    - Tip: Prefer the post-export fixer when possible; the - build phase is only a fallback for projects that cannot run the script - after export. -
    -

    - -
    -

    - Verify Installation - - # - -

    - Add GodotIapWrapper as a child node in your scene, then - use this script: + Prefer the post-export fixer when possible; the build phase is only a + fallback for projects that cannot run the script after export.

    - - {`extends Node - -const Types = preload("res://addons/godot-iap/types.gd") - -@onready var iap = $GodotIapWrapper - -func _ready(): - print("Godot IAP is available!") - - # Connect signals - iap.connected.connect(_on_connected) - iap.purchase_updated.connect(_on_purchase_updated) - iap.purchase_error.connect(_on_purchase_error) - - # Initialize connection - var success = iap.init_connection() - print("Init result: ", success) - -func _on_connected(): - print("Store connected!") - -func _on_purchase_updated(purchase: Dictionary): - print("Purchase: %s" % purchase.get("product_id", "")) - -func _on_purchase_error(error: Dictionary): - print("Error: ", error)`} - - -
    - Note: The native plugin is only available on iOS and - Android in the default release zip. In the editor and on desktop - platforms, store calls return fallback values or no-op results so you - can test your integration flow without opening a real store - connection. -
    @@ -461,6 +388,53 @@ func _ready():
    +
    +

    + Verify Installation + + # + +

    +

    + With the GodotIapWrapper node from Scene Setup in place, + attach this script to confirm the plugin loads and the store connects: +

    + + {`extends Node + +const Types = preload("res://addons/godot-iap/types.gd") + +@onready var iap = $GodotIapWrapper + +func _ready(): + print("Godot IAP is available!") + + # Connect signals + iap.connected.connect(_on_connected) + iap.purchase_updated.connect(_on_purchase_updated) + iap.purchase_error.connect(_on_purchase_error) + + # Initialize connection + var success = await iap.init_connection() + print("Init result: ", success) + +func _on_connected(): + print("Store connected!") + +func _on_purchase_updated(purchase: Dictionary): + print("Purchase: %s" % purchase.get("product_id", "")) + +func _on_purchase_error(error: Dictionary): + print("Error: ", error)`} + + + The native plugin is only available on iOS and Android in the default + release zip. In the editor and on desktop platforms, store calls + return fallback values or no-op results so you can test your + integration flow without opening a real store connection. + +
    +

    Usage @@ -468,6 +442,46 @@ func _ready(): #

    +

    + The typical flow is{' '} + + init_connection + {' '} + → connect the{' '} + + purchase_updated + {' '} + and{' '} + + purchase_error + {' '} + signals →{' '} + + fetch_products + {' '} + →{' '} + + request_purchase + {' '} + →{' '} + + finish_transaction + + . See the Purchase Guide for the + complete flow. +

    +

    + The examples below use the iap reference created in Scene + Setup (@onready var iap = $GodotIapWrapper). +

    + +

    + GDScript uses snake_case for all function names ( + init_connection, fetch_products,{' '} + request_purchase). Return types use Array{' '} + for lists and Variant for platform-specific single + results. +

    Signal-Based Architecture @@ -484,34 +498,48 @@ func _ready(): func _ready(): # Connect signals - GodotIapPlugin.purchase_updated.connect(_on_purchase_updated) - GodotIapPlugin.purchase_error.connect(_on_purchase_error) + iap.purchase_updated.connect(_on_purchase_updated) + iap.purchase_error.connect(_on_purchase_error) # Initialize - await GodotIapPlugin.init_connection() + await iap.init_connection() func _on_purchase_updated(purchase): # Validate receipt with your backend or IAPKit, then: - await GodotIapPlugin.finish_transaction(purchase, true) + # (second argument: true only for consumable products) + await iap.finish_transaction(purchase, false) print("Purchased: ", purchase.product_id) func _on_purchase_error(error): print("Error: ", error.code, " - ", error.message)`} +

    + Each call here has a full reference — see the{' '} + + purchase_updated + {' '} + and{' '} + + purchase_error + {' '} + signals,{' '} + + init_connection + + , and{' '} + + finish_transaction + {' '} + for parameters and per-store behavior, and{' '} + Error Codes for the error.code{' '} + values. +

    -
    - Critical: Always call finishTransaction{' '} - after verifying a purchase. On Android, unfinished purchases are - automatically refunded after 3 days. -
    + + Always call finish_transaction after verifying a + purchase. On Android, unfinished purchases are automatically refunded + after 3 days. +

    Fetching Products @@ -519,18 +547,33 @@ func _on_purchase_error(error): #

    +

    + Query store metadata with a{' '} + + ProductRequest + + ; results are platform-typed (ProductAndroid on Android,{' '} + ProductIOS on iOS): +

    {`func _load_products(): var request = Types.ProductRequest.new() request.skus = ["premium", "coins_100"] request.type = Types.ProductQueryType.InApp - var products: Array = await GodotIapPlugin.fetch_products(request) + var products: Array = await iap.fetch_products(request) for product in products: # On Android: Types.ProductAndroid # On iOS: Types.ProductIOS print(product.id, " - ", product.display_price)`} +

    + See{' '} + + fetch_products + {' '} + for parameters and per-store behavior. +

    Making a Purchase @@ -538,6 +581,11 @@ func _on_purchase_error(error): #

    +

    + Configure both platforms in a single request — the plugin picks the + branch matching the store the game is running on, so one purchase call + works everywhere: +

    {`func _purchase(sku: String): var platforms = Types.RequestPurchasePropsByPlatforms.new() @@ -548,24 +596,59 @@ func _on_purchase_error(error): platforms.google.skus = skus var props = Types.RequestPurchaseProps.in_app(platforms) # Returns Variant (PurchaseAndroid or PurchaseIOS, or null) - var purchase = await GodotIapPlugin.request_purchase(props)`} + var purchase = await iap.request_purchase(props)`} +

    + See{' '} + + request_purchase + {' '} + for parameters and per-store behavior. +

    +
    -
    - Note: GDScript uses snake_case for all - function names (init_connection,{' '} - fetch_products, request_purchase). Return - types use Array for lists and Variant for - platform-specific single results. -
    +
    +

    + Troubleshooting + + # + +

    +

    + Products not found + + # + +

    +
      +
    • + Ensure all agreements are signed in App Store Connect / Google Play + Console +
    • +
    • + Verify banking, legal, and tax information is complete and approved +
    • +
    • Check that bundle ID / package name matches exactly
    • +
    • + Products must be in "Ready to Submit" status (Apple) or "Active" + (Google) +
    • +
    • Wait 15-30 minutes after creating products before testing
    • +
    + +

    + App crashes on launch (iOS) + + # + +

    +

    + A crash with{' '} + Library not loaded: @rpath/GodotIap.framework/GodotIap{' '} + means the frameworks were not embedded — see{' '} + iOS: Xcode Framework Embedding and run{' '} + fix_ios_embed.sh. +

    @@ -593,8 +676,11 @@ func _on_purchase_error(error): multi-language examples
  • - Store Setup — Horizon OS, Fire OS, - Vega OS support boundaries, and store target configuration + Store Setup — support boundaries for + alternative stores such as{' '} + Horizon OS (Meta Quest) and{' '} + Fire OS / Vega OS (Amazon); + godot-iap does not yet ship dedicated flavors for these targets
  • - -
    -

    - Troubleshooting - - # - -

    -

    Products not found

    -
      -
    • - Ensure all agreements are signed in App Store Connect / Google Play - Console -
    • -
    • - Verify banking, legal, and tax information is complete and approved -
    • -
    • Check that bundle ID / package name matches exactly
    • -
    • - Products must be in "Ready to Submit" status (Apple) or "Active" - (Google) -
    • -
    • Wait 15-30 minutes after creating products before testing
    • -
    -
    ); } diff --git a/packages/docs/src/pages/docs/setup/kmp.tsx b/packages/docs/src/pages/docs/setup/kmp.tsx index 7cb519fa5..a0cfaa13f 100644 --- a/packages/docs/src/pages/docs/setup/kmp.tsx +++ b/packages/docs/src/pages/docs/setup/kmp.tsx @@ -1,3 +1,4 @@ +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; import { LIBRARIES } from '../../../lib/images'; @@ -20,25 +21,19 @@ function KmpSetup() { />

    Kotlin Multiplatform Setup

    - kmp-iap provides in-app purchase support for Kotlin - Multiplatform projects. It supports Android natively and iOS via - CocoaPods integration. + kmp-iap brings OpenIAP-compliant in-app purchases to Kotlin + Multiplatform projects. Android talks to Google Play Billing directly; + iOS links the OpenIAP StoreKit framework, added with either CocoaPods or + Swift Package Manager (see iOS Configuration{' '} + below). Requires iOS 15.0+ and the Android minSdk shown in{' '} + Android Configuration.

    -
    - Before you start: Complete the store configuration - before integrating with your framework:{' '} + + Complete the store configuration before integrating with your framework:{' '} iOS Setup |{' '} Android Setup -
    +

    @@ -113,20 +108,11 @@ dependencies { CocoaPods or Swift Package Manager:

    -
    - Quick decision: Use CocoaPods if you - want automatic dependency management through Gradle. Use{' '} - SPM if you prefer modern iOS tooling and want to - avoid CocoaPods. -
    + + Use CocoaPods if you want automatic dependency + management through Gradle. Use SPM if you prefer + modern iOS tooling and want to avoid CocoaPods. +

    Option A: CocoaPods (Recommended)

    Ensure your shared module has the CocoaPods plugin:

    @@ -152,7 +138,6 @@ kotlin { Then run cd iosApp && pod install and always open{' '} .xcworkspace (not .xcodeproj).

    -

    Option B: Swift Package Manager

    1. @@ -169,18 +154,10 @@ kotlin {
    -
    - Note: With SPM, don't use the CocoaPods plugin. - You'll need to manually update OpenIAP when kmp-iap updates. -
    + + With SPM, don't use the CocoaPods plugin. You'll need to manually + update OpenIAP when kmp-iap updates. +

    Enable In-App Purchase Capability

    @@ -188,9 +165,11 @@ kotlin { + Capability > In-App Purchase

    -

    Configure Info.plist (iOS 14+)

    +

    Configure Info.plist (optional)

    - Add to your iosApp/Info.plist: + Declaring itms-apps in iosApp/Info.plist is + only needed when your own code checks App Store links before opening + them — kmp-iap itself does not require it:

    {`LSApplicationQueriesSchemes @@ -235,6 +214,36 @@ kotlin { # +

    + Results stream through Kotlin Flows: initialize the connection ( + + initConnection + + ), attach the purchase and error flows ( + + purchaseUpdatedListener + {' '} + and{' '} + + purchaseErrorListener + + ), then fetch and purchase ( + + fetchProducts + + ,{' '} + + requestPurchase + + ,{' '} + + finishTransaction + + ). A successful initConnection() followed by a non-empty{' '} + fetchProducts() result is the quickest way to confirm the + platform setup above is working. For the full flow, see the{' '} + Purchase Guide. +

    Creating an Instance @@ -242,17 +251,27 @@ kotlin { #

    -

    Two patterns are supported:

    +

    + Two patterns are supported. The connection, fetch, and purchase APIs + are suspend functions — call them from a coroutine scope: +

    {`// Option 1: Global instance (convenient) import io.github.hyochan.kmpiap.kmpIapInstance -kmpIapInstance.initConnection() +scope.launch { kmpIapInstance.initConnection() } // Option 2: Constructor (for DI / testing) import io.github.hyochan.kmpiap.KmpIAP val kmpIAP = KmpIAP() -kmpIAP.initConnection()`} +scope.launch { kmpIAP.initConnection() }`} +

    + See{' '} + + initConnection + {' '} + for parameters and per-store behavior. +

    Flow-Based Architecture @@ -261,7 +280,18 @@ kmpIAP.initConnection()`}

    - KMP IAP uses Kotlin Flow for purchase events: + KMP IAP delivers purchase results through hot Kotlin{' '} + Flows — despite the names,{' '} + + purchaseUpdatedListener + {' '} + and{' '} + + purchaseErrorListener + {' '} + are Flows, not one-shot callbacks. Collect both in a long-lived + coroutine scope before requesting a purchase; events are emitted as + they occur.

    {`import io.github.hyochan.kmpiap.KmpIAP @@ -270,7 +300,7 @@ import kotlinx.coroutines.* val kmpIAP = KmpIAP() val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) -kmpIAP.initConnection() +scope.launch { kmpIAP.initConnection() } // Collect in separate coroutines (collect is suspending and never returns) scope.launch { @@ -287,20 +317,19 @@ scope.launch { } }`} +

    + For the possible error.code values, see{' '} + Error Codes. +

    -
    - Critical: Always call finishTransaction{' '} + + Always call{' '} + + finishTransaction + {' '} after verifying a purchase. On Android, unfinished purchases are automatically refunded after 3 days. -
    +

    Products and Purchase @@ -308,96 +337,58 @@ scope.launch { #

    +

    + Fetch products once the connection is up, then request a purchase — + the result arrives on purchaseUpdatedListener, not as a + return value. See{' '} + + fetchProducts + {' '} + and{' '} + + requestPurchase + {' '} + for parameters and per-store behavior. +

    {`import io.github.hyochan.kmpiap.openiap.* -// Fetch products -val products = kmpIAP.fetchProducts( - ProductRequest( - skus = listOf("premium", "coins_100"), - type = ProductQueryType.InApp +scope.launch { + // Fetch products + val products = kmpIAP.fetchProducts( + ProductRequest( + skus = listOf("premium", "coins_100"), + type = ProductQueryType.InApp + ) ) -) - -// Purchase -kmpIAP.requestPurchase( - RequestPurchaseProps( - request = RequestPurchaseProps.Request.Purchase( - RequestPurchasePropsByPlatforms( - google = RequestPurchaseAndroidProps( - skus = listOf("premium") + + // Purchase — the result arrives on purchaseUpdatedListener + kmpIAP.requestPurchase( + RequestPurchaseProps( + request = RequestPurchaseProps.Request.Purchase( + RequestPurchasePropsByPlatforms( + apple = RequestPurchaseIosProps( + sku = "premium" + ), + google = RequestPurchaseAndroidProps( + skus = listOf("premium") + ) ) - ) - ), - type = ProductQueryType.InApp + ), + type = ProductQueryType.InApp + ) ) -) - -// Cleanup -kmpIAP.endConnection()`} +}`} - -
    - Note: KMP IAP uses Kotlin Flow (not callbacks or - listeners). Collect purchase and error flows in a coroutine scope. The - flows are hot and emit events as they occur. -
    -
    - -
    -

    - Next Steps - - # - -

    - +

    + Call{' '} + + endConnection() + {' '} + when the owning screen or scope is disposed — not right after{' '} + requestPurchase, or the connection may close before the + purchase result arrives. +

    @@ -450,6 +441,56 @@ kmpIAP.endConnection()`}
    + +
    +

    + Next Steps + + # + +

    + +
    ); } diff --git a/packages/docs/src/pages/docs/setup/maui.tsx b/packages/docs/src/pages/docs/setup/maui.tsx index e81616556..d5030984c 100644 --- a/packages/docs/src/pages/docs/setup/maui.tsx +++ b/packages/docs/src/pages/docs/setup/maui.tsx @@ -1,3 +1,4 @@ +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import SEO from '../../../components/SEO'; import { MAUI_PACKAGE } from '../../../lib/versioning'; @@ -19,28 +20,17 @@ function MauiSetup() { verified them.

    -
    +

    - Package shape: apps reference only{' '} - OpenIap.Maui. OpenIAP-owned Android and iOS binding - outputs are flattened into that package. Google Billing, Play - Services, Gson, AndroidX, and Kotlin Android libraries stay as normal - NuGet dependencies so your app can deduplicate them with its own - package graph. -

    -
    - -
    -

    - Before you start: create the products in App Store - Connect and Google Play Console first. The product IDs in your MAUI - app must exactly match the store product IDs. + Create the products in App Store Connect and Google Play Console + first. The product IDs in your MAUI app must exactly match the store + product IDs.

    Platform setup guides: iOS Setup |{' '} Android Setup

    -
    +

    @@ -73,9 +63,6 @@ function MauiSetup() { iOS 15+ / macCatalyst 15+, Apple Developer account, matching bundle identifier, In-App Purchase capability, sandbox tester. - OpenIap.Maui 2.0.0 embeds an Apple XCFramework built with Xcode - 27; custom source builds need Xcode 27 to include the guarded - StoreKit 27 implementation. @@ -121,18 +108,35 @@ function MauiSetup() { `} -
    -

    - Working from this monorepo before publishing? Use a - project reference to the main project only. The example app - re-declares local native references because MSBuild does not - propagate those transitively through ProjectReference. - Published NuGet consumers do not need that. -

    - - {``} - -
    +

    + Your app references a single package, OpenIap.Maui. The + OpenIAP-owned iOS and Android bindings are bundled inside it, while + shared dependencies (Google Play Billing, Play Services, AndroidX, + Kotlin, Gson) remain ordinary NuGet dependencies so NuGet can + deduplicate them with the rest of your dependency graph. +

    + +

    + If you are working from this monorepo before publishing, use a project + reference to the main project only. The example app re-declares local + native references because MSBuild does not propagate those + transitively through ProjectReference. Published NuGet + consumers do not need that. +

    + + {``} + +

    + Building the Apple library from source requires Xcode 27; the + published package already embeds a prebuilt XCFramework, so NuGet + consumers do not need Xcode 27 — their normal toolchain is enough. +

    + + + OpenIap.Maui 2.x supports .NET 10 only. Retarget every{' '} + net9.0-* TFM to the matching net10.0-* TFM + and update the MAUI workload before upgrading the package. +

    @@ -142,6 +146,10 @@ function MauiSetup() { # +

    + Configure the app project once per platform before calling any store + API. +

    Target Frameworks @@ -216,6 +224,34 @@ function MauiSetup() { #

    +

    + The typical flow is: initialize the connection with{' '} + + InitConnectionAsync + + , fetch products with{' '} + + FetchProductsAsync + + , register the{' '} + + PurchaseUpdated + {' '} + and{' '} + + PurchaseError + {' '} + listeners, request a purchase with{' '} + + RequestPurchaseAsync + + , then finish the verified transaction with{' '} + + FinishTransactionAsync + + . The Purchase Guide covers the + complete flow. +

    Initialize and Fetch Products @@ -247,6 +283,17 @@ var products = result is FetchProductsResultProducts { Value: { } list } ? list : Array.Empty();`} +

    + Each call here has a full reference — see{' '} + + InitConnectionAsync + {' '} + and{' '} + + FetchProductsAsync + {' '} + for parameters and per-store behavior. +

    Listen Before Requesting a Purchase @@ -256,8 +303,25 @@ var products = result is FetchProductsResultProducts { Value: { } list }

    Purchase APIs are event-based. Register listeners before calling{' '} - RequestPurchaseAsync, then finish the transaction only - after server-side verification succeeds. + + RequestPurchaseAsync + + , then finish the transaction only after server-side verification + succeeds. See{' '} + + PurchaseUpdated + + ,{' '} + + PurchaseError + + , and{' '} + + FinishTransactionAsync + {' '} + for parameters and per-store behavior, and{' '} + Error Codes for the error.Code{' '} + values.

    {`IDisposable purchaseSub = iap.PurchaseUpdated.Subscribe(async purchase => @@ -295,14 +359,11 @@ await mutate.RequestPurchaseAsync(new RequestPurchaseProps });`} -
    -

    - Do not skip finishing transactions. On Android, - unfinished purchases are refunded automatically after 3 days. On - iOS, unfinished transactions can be delivered again on the next app - launch. -

    -
    + + On Android, unfinished purchases are refunded automatically after 3 + days. On iOS, unfinished transactions can be delivered again on the + next app launch. +

    IAPKit API @@ -311,11 +372,15 @@ await mutate.RequestPurchaseAsync(new RequestPurchaseProps

    - The MAUI package exposes the same IAPKit helper surface as the - JavaScript SDKs: create a kit client for status, entitlements, and - bind-user calls. These app-facing calls use the publishable key. Store - lifecycle webhooks flow into IAPKit only; there is no outbound webhook - stream in the MAUI package. + IAPKit is OpenIAP's hosted receipt-validation backend (see{' '} + Purchase Verification with IAPKit). + The MAUI package ships the same app-facing helper as the other OpenIAP + SDKs: create a kit client with your publishable key to read purchase + status and entitlements and to bind a purchase to a user. Store + lifecycle events (App Store Server Notifications, Google Play RTDN) + are delivered to IAPKit's backend, not to your app — the client reads + current state through these bounded calls rather than subscribing to a + webhook stream.

    {`using OpenIap; @@ -340,7 +405,11 @@ BindUserResponse bind = await kit.BindUserAsync(purchase.PurchaseToken!, "user_1

    Dispose subscriptions and close the store connection when the page or - service that owns the purchase flow is torn down. + service that owns the purchase flow is torn down. See{' '} + + EndConnectionAsync + {' '} + for its full reference.

    {`purchaseSub.Dispose(); @@ -364,12 +433,23 @@ await mutate.EndConnectionAsync();`} Flow, Subscription Flow, Available Purchases, Offer Code, Alternative Billing.

    +

    + The example app builds against the in-repo Android library, so rebuild + the OpenIAP Android AARs once before the first run (published-package + consumers skip this step): +

    {`# From the OpenIAP repo root: (cd packages/google && ./gradlew :openiap:assemblePlayRelease) -(cd libraries/maui-iap/android && ../../../packages/google/gradlew :openiap:assembleRelease) - -cd libraries/maui-iap/example/OpenIap.Maui.Example +(cd libraries/maui-iap/android && ../../../packages/google/gradlew :openiap:assembleRelease)`} + +

    + Then run the example (its application id is{' '} + dev.hyo.martie; uninstalling first clears stale native + code): +

    + + {`cd libraries/maui-iap/example/OpenIap.Maui.Example # Android device or emulator adb uninstall dev.hyo.martie || true @@ -381,14 +461,6 @@ dotnet build -t:Run -f net10.0-ios # macCatalyst dotnet build -t:Run -f net10.0-maccatalyst`} -
    -

    - Upgrading from OpenIap.Maui 1.x? OpenIap.Maui 2.x - supports .NET 10 only. Retarget every net9.0-* TFM to - the matching net10.0-* TFM and update the MAUI workload - before upgrading the package. -

    -

    VS Code launch configurations are available in{' '} libraries/maui-iap/.vscode/launch.json. The iOS device @@ -406,6 +478,9 @@ dotnet build -t:Run -f net10.0-maccatalyst`} # +

    + Common failures and their causes, roughly in the order you hit them. +

    Products Do Not Load @@ -416,7 +491,10 @@ dotnet build -t:Run -f net10.0-maccatalyst`}
    • Confirm the store product IDs exactly match the IDs passed in{' '} - ProductRequest.Skus. + + ProductRequest + + .Skus.
    • Confirm the bundle identifier or Android package name matches the @@ -441,9 +519,18 @@ dotnet build -t:Run -f net10.0-maccatalyst`}

      If Google Play shows "This version of the application is not configured for billing through Google Play", the library reached - BillingClient correctly. The app build is not accepted by Play Billing - for that package, signing key, track, tester, or product setup yet. + BillingClient correctly - Play itself rejected the app build. Check + each of the following:

      +
        +
      • + The installed build's package name and signing key match the + uploaded build. +
      • +
      • The build is on an internal or closed test track.
      • +
      • The signed-in account is a license tester.
      • +
      • The products are active in Play Console.
      • +

      Android Old BillingClient Error @@ -485,9 +572,12 @@ dotnet build -t:Run -f net10.0-android`}

      Verify the app is running on a signed device build with the matching bundle identifier and In-App Purchase capability. If you navigate away - from a purchase screen, call EndConnectionAsync from the - owning page or lifecycle service so the next screen can initialize a - fresh store connection. + from a purchase screen, call{' '} + + EndConnectionAsync + {' '} + from the owning page or lifecycle service so the next screen can + initialize a fresh store connection.

    @@ -514,8 +604,11 @@ dotnet build -t:Run -f net10.0-android`} API Reference - generated API reference
  • - Store Setup - Horizon OS, Fire OS, - Vega OS support boundaries, and store target configuration + Store Setup - shipping beyond Apple + and Google: Amazon Appstore{' '} + (Fire OS and Vega OS devices) and{' '} + Meta Quest (Horizon OS){' '} + support boundaries and store target configuration
  • React Native Setup

    react-native-iap provides in-app purchase support for React - Native apps using Nitro Modules. It supports StoreKit 2 on iOS and - Google Play Billing {GOOGLE_PLAY_BILLING.version}+ on Android by - default, with optional Horizon and Fire OS Android flavors. + Native apps using Nitro Modules, a high-performance native bridging + layer for React Native. It supports StoreKit 2 on iOS and Google Play + Billing {GOOGLE_PLAY_BILLING.version}+ on Android by default, with + optional build flavors for{' '} + Horizon OS (Meta Quest) and{' '} + Fire OS (Amazon Appstore).

    -
    - For bare React Native CLI projects only. Starting from - v15.0.0, react-native-iap no longer supports Expo. If you're using Expo, - use expo-iap instead — it provides the - same API with Expo modules architecture. -
    - -
    - Before you start: Complete the store configuration - before integrating with your framework:{' '} +

    + react-native-iap v15+ is for bare React Native CLI{' '} + projects only and requires React Native 0.79+ (Nitro + Modules), iOS 15.0+ (StoreKit 2), and{' '} + Android minSdkVersion {ANDROID_SDK.minSdk}+ with{' '} + compileSdkVersion {ANDROID_SDK.compileSdk}+. Expo + support ended in v15.0.0 — use expo-iap{' '} + instead; it provides the same API on Expo Modules. +

    + + + Complete the store configuration before integrating with your framework:{' '} iOS Setup |{' '} Android Setup -
    +

    @@ -58,28 +48,17 @@ function ReactNativeSetup() {

    -
    - Compatibility: react-native-iap v15+ - uses Nitro Modules and requires React Native 0.79+. - It is designed for bare React Native CLI projects - only. If you're using Expo, use{' '} - expo-iap instead. -
    +

    + react-native-iap requires react-native-nitro-modules as a + peer dependency — install both together. +

    {`# Using yarn (recommended) -yarn add react-native-iap +yarn add react-native-iap react-native-nitro-modules # Using npm -npm install react-native-iap`} +npm install react-native-iap react-native-nitro-modules`}

    @@ -91,7 +70,7 @@ npm install react-native-iap`}

    react-native-iap v15+ is built on{' '} @@ -108,49 +87,12 @@ npm install react-native-iap`} Native modules are automatically linked during your app's build process

  • -
  • - If you encounter Swift 6 C++ interop errors in - Nitro (e.g., AnyMap.swift using{' '} - cppPart.pointee.*), pin Swift 5.10 for the{' '} - NitroModules pod as a temporary workaround: -
  • - - {`// ios/Podfile - add inside post_install block -post_install do |installer| - installer.pods_project.targets.each do |target| - if target.name == 'NitroModules' - target.build_configurations.each do |config| - config.build_settings['SWIFT_VERSION'] = '5.0' - end - end - end -end`} - - -
    - Recommended path: Upgrade to RN 0.79+, update{' '} - react-native-nitro-modules and nitro-codegen{' '} - to latest, then pod install and do a clean build. If - issues persist, share a minimal repro (package.json +{' '} - Podfile) on{' '} - - GitHub Issues - - . -
    +

    + If you hit Swift 6 C++ interop errors in Nitro, see{' '} + Troubleshooting for the Swift 5 + language mode workaround. +

    iOS @@ -159,9 +101,6 @@ end`}

      -
    • - Requires iOS 15.0+ (StoreKit 2) -
    • Install CocoaPods: @@ -191,20 +130,19 @@ end`}
        -
      • - Requires minSdkVersion {ANDROID_SDK.minSdk}+ and{' '} - compileSdkVersion {ANDROID_SDK.compileSdk}+ -
      • No additional native configuration needed
      • Uses Google Play Billing {GOOGLE_PLAY_BILLING.version}+ with automatic service reconnection
      • - Store-specific Android targets such as Horizon OS, Fire OS, and Vega - OS have separate build artifacts. Keep the React Native setup here, - then use Store Setup for target-specific Gradle, manifest, and - runtime details. + Store-specific Android targets —{' '} + Horizon OS (Meta Quest) and{' '} + Fire OS (Amazon Appstore) — + ship as separate build flavors. Complete the setup on this page + first, then follow Store Setup for + target-specific Gradle, manifest, and runtime details. Vega OS is + not an Android flavor — see Vega OS below.
      @@ -215,10 +153,14 @@ end`}

      - Bare React Native does not use an Expo config plugin. For Vega OS, - keep Amazon Kepler packages in a Vega-only React Native target and - follow Store Setup for package, manifest, and supported-version - details. + Vega OS is Amazon's newer, non-Android operating system; its apps run + on the Kepler runtime. react-native-iap supports it through a separate + React Native for Vega target — it is not an Android build flavor, and + unlike expo-iap there is no config plugin to enable it. Keep the + Amazon Kepler packages in that Vega-only target so regular iOS and + Android builds are unaffected, and follow{' '} + Amazon Store Setup for package, + manifest, and supported-version details.

      @@ -230,6 +172,41 @@ end`} +

      + Under the hood, the typical flow is{' '} + + initConnection + {' '} + → set up{' '} + + purchaseUpdatedListener + {' '} + and{' '} + + purchaseErrorListener + {' '} + →{' '} + + fetchProducts + {' '} + →{' '} + + requestPurchase + {' '} + →{' '} + + finishTransaction + + , with{' '} + + endConnection + {' '} + on teardown. The useIAP hook manages the connection and + listener steps for you. See the{' '} + Purchase Guide for the complete + flow. +

      +

      useIAP Hook (Recommended) @@ -292,20 +269,38 @@ function Store() { }`} -
      - Important: Most useIAP methods return{' '} - Promise<void> and update internal state. Use{' '} +

      + Each call here has a full reference — see{' '} + + fetchProducts + + ,{' '} + + requestPurchase + + , and{' '} + + finishTransaction + {' '} + for parameters and per-store behavior, and{' '} + + ErrorCode + {' '} + for the full error reference. +

      + + + Always call finishTransaction after verifying a purchase. + On Android, unfinished purchases are automatically refunded after 3 + days. + + +

      + Most useIAP methods return{' '} + Promise<void> and update internal state — use the{' '} onPurchaseSuccess callback to receive purchase results, not the return value of requestPurchase. -

      +

      Hook State @@ -316,23 +311,28 @@ function Store() {

      After calling methods, consume state from the hook:

      • - products — Populated after{' '} + products (Product[]) + — Populated after{' '} fetchProducts()
      • - subscriptions — Populated after fetching with type{' '} - 'subs' + subscriptions ( + ProductSubscription + []) — Populated after fetching with type 'subs'
      • - availablePurchases — Populated after{' '} + availablePurchases ( + Purchase[]) — Populated after{' '} getAvailablePurchases()
      • - activeSubscriptions — Populated after{' '} + activeSubscriptions ( + ActiveSubscription[]) + — Populated after{' '} getActiveSubscriptions() @@ -392,6 +392,37 @@ purchaseSub.remove(); errorSub.remove(); await endConnection();`} +

        + Each call here has a full reference — see{' '} + + initConnection + + ,{' '} + + fetchProducts + + ,{' '} + + requestPurchase + + ,{' '} + + finishTransaction + + , and{' '} + + endConnection + {' '} + for parameters and per-store behavior, and{' '} + + purchaseUpdatedListener + {' '} + and{' '} + + purchaseErrorListener + {' '} + for the purchase events. +

        @@ -433,54 +464,6 @@ switch (error.code) {
        -
        -

        - Next Steps - - # - -

        - -
        -

        Troubleshooting @@ -529,7 +512,7 @@ switch (error.code) { RCT-Folly/folly/Expected.h, add these defines to your{' '} Podfile post_install block:

        - + {`post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| @@ -545,10 +528,89 @@ end`}

        Swift 6 C++ interop errors (Nitro)

        If you see errors in AnyMap.swift related to{' '} - cppPart.pointee, see the{' '} - Nitro Modules section above for the Swift - 5.10 pin workaround. + cppPart.pointee, switch the NitroModules pod + to the Swift 5 language mode (SWIFT_VERSION = '5.0') as a + temporary workaround:

        + + {`# ios/Podfile - add inside post_install block +post_install do |installer| + installer.pods_project.targets.each do |target| + if target.name == 'NitroModules' + target.build_configurations.each do |config| + config.build_settings['SWIFT_VERSION'] = '5.0' + end + end + end +end`} + + + + Upgrade to RN 0.79+, update react-native-nitro-modules{' '} + and nitro-codegen to latest, then{' '} + pod install and do a clean build. If issues persist, + share a minimal repro (package.json +{' '} + Podfile) on{' '} + + GitHub Issues + + . + +

        + +
        +

        + Next Steps + + # + +

        +
        ); diff --git a/packages/docs/src/pages/docs/setup/store/amazon.tsx b/packages/docs/src/pages/docs/setup/store/amazon.tsx index 74ca55379..61927978d 100644 --- a/packages/docs/src/pages/docs/setup/store/amazon.tsx +++ b/packages/docs/src/pages/docs/setup/store/amazon.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import SEO from '../../../../components/SEO'; import { useScrollToHash } from '../../../../hooks/useScrollToHash'; @@ -17,25 +18,68 @@ function AmazonStoreSetup() { keywords="Amazon Fire OS IAP, Vega OS IAP, openiap-google-amazon, expo-iap amazon, react-native-iap Vega, Amazon Appstore SDK" />

        Amazon Store Setup

        -
        - Experimental RC support: Fire OS and Vega OS support is - available from the current next / rc package - versions while the integration is being finalized. -
        + + Fire OS and Vega OS support is available from the current{' '} + next / rc package versions while the + integration is being finalized. +

        - Amazon support has two separate targets. Fire OS is an Android Appstore - build using the amazon Gradle flavor. Vega OS is a Kepler - runtime target for React Native for Vega and compatible Expo Vega - builds. Do not use fireOsEnabled as a Vega selector. + Amazon distributes apps to two different device families, and OpenIAP + treats them as two separate targets. Fire OS is Amazon's Android-based + OS (Fire TV, Fire tablets): an Amazon Appstore build is a regular + Android build that selects the amazon Gradle flavor. Vega + OS is Amazon's newer, non-Android OS: its apps run on the Kepler + JavaScript runtime and are built with React Native for Vega (or a + compatible Expo Vega build), so Vega is a separate project target rather + than an Android flavor.

        +
        + + Target Model + +

        + OpenIAP selects each Amazon target through a different mechanism — a + Gradle flavor for Fire OS, a runtime-selected adapter for Vega OS: +

        + + + + + + + + + + + + + + + + + + + + +
        TargetRuntimeOpenIAP selection
        Fire OSAndroid / Amazon Appstore SDK + Android amazon flavor and{' '} + openiap-google-amazon. +
        Vega OSAmazon Kepler JavaScript runtime + Runtime-selected kepler adapter in{' '} + expo-iap or react-native-iap. +
        +
        +
        Required Values

        Fire OS and Vega OS both validate through Amazon receipts, but their - app metadata is configured in different places. + app metadata is configured in different places. App Tester refers to + Amazon App Tester, Amazon's sideloaded sandbox app for exercising test + purchases on device.

        @@ -109,43 +153,16 @@ function AmazonStoreSetup() {
        -
        - - Target Model - - - - - - - - - - - - - - - - - - - - - -
        TargetRuntimeOpenIAP selection
        Fire OSAndroid / Amazon Appstore SDK - Android amazon flavor and{' '} - openiap-google-amazon. -
        Vega OSAmazon Kepler JavaScript runtime - Runtime-selected kepler adapter in{' '} - expo-iap or react-native-iap. -
        -
        -
        Framework Setup +

        + The table below summarizes how each framework selects an Amazon + target; the Fire OS and{' '} + Vega OS sections that follow contain the full + configuration. +

        @@ -231,6 +248,10 @@ function AmazonStoreSetup() { Native Android +

        + Depend on the Amazon artifact and select the amazon{' '} + flavor in the app's Gradle build: +

        {`dependencies { implementation("io.github.hyochan.openiap:openiap-google-amazon:${OPENIAP_VERSIONS.google}") } @@ -265,8 +286,11 @@ android { React Native

        - Bare React Native selects Fire OS at the Gradle layer. There is no RN - config plugin for Amazon options. + Bare React Native selects Fire OS at the Gradle layer; there is no RN + config plugin for Amazon options. The same switch also drives Horizon + OS (Meta Quest) builds — see{' '} + Horizon OS Setup — so keep{' '} + horizonEnabled=false in Amazon artifacts.

        {`# android/gradle.properties fireOsEnabled=true @@ -287,10 +311,15 @@ android { Flutter +

        + Flutter uses the same Gradle property model as bare React Native — set + the property, then map it to the plugin flavor in the app module: +

        {`# android/gradle.properties fireOsEnabled=true horizonEnabled=false`} - {`def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false + {`// android/app/build.gradle +def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false def flavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play') @@ -304,7 +333,7 @@ android { KMP and MAUI

        - KMP exposes the Android amazonRelease variant and sets + KMP exposes the Android amazonRelease variant and sets{' '} OPENIAP_STORE="amazon". MAUI selects the Amazon AAR flavor by MSBuild property.

        @@ -318,8 +347,13 @@ dotnet build -f net10.0-android -p:OpenIapAndroidStore=amazon`}

        Vega OS is not an Android flavor. Keep Vega dependencies isolated in a - Vega target so regular iOS, Android, Fire OS, and Horizon builds do - not install Amazon Kepler packages. + Vega target so regular iOS, Android, Fire OS, and{' '} + Horizon builds do not + install Amazon Kepler packages. +

        +

        + fireOsEnabled selects the Fire OS Android flavor only — + it is not a Vega selector and has no effect on Vega builds.

        Check the{' '} @@ -339,12 +373,9 @@ dotnet build -f net10.0-android -p:OpenIapAndroidStore=amazon`} Expo

        - Expo can prepare the Vega target from config. fireOS and - vegaOS can both be present, but they still produce - separate artifacts. Keep the enable flags in{' '} - modules.amazon; use android.amazon.vegaOS{' '} - only when Vega metadata needs to differ from the normal Expo app - config. + Expo can prepare the Vega target from config. fireOS and{' '} + vegaOS can both be enabled, but they still produce + separate artifacts; keep both flags in modules.amazon.

        {`plugins: [ [ @@ -404,10 +435,17 @@ id = "/com.amazon.kepler.appstore.iap.purchase.core@IAppstoreIAPPurchaseCoreServ Verification

        - Fire OS and Vega OS both use the IAPKit Amazon payload. Pass the + Fire OS and Vega OS both use the{' '} + IAPKit Amazon payload. Pass the Amazon user id when available, the Amazon receipt id, and{' '} sandbox: true for Amazon App Tester validation.

        +

        + The example below uses the TypeScript SDKs (expo-iap,{' '} + react-native-iap); other frameworks pass the same{' '} + iapkit.amazon payload through their own{' '} + verifyPurchaseWithProvider call. +

        {`await verifyPurchaseWithProvider({ provider: 'iapkit', iapkit: { diff --git a/packages/docs/src/pages/docs/setup/store/horizon.tsx b/packages/docs/src/pages/docs/setup/store/horizon.tsx index a748a186e..98a5d76e2 100644 --- a/packages/docs/src/pages/docs/setup/store/horizon.tsx +++ b/packages/docs/src/pages/docs/setup/store/horizon.tsx @@ -18,10 +18,11 @@ function HorizonStoreSetup() { />

        Horizon OS Store Setup

        - Horizon OS support is an Android build target for Meta Quest devices. - Select the horizon platform flavor, provide the Horizon app - id from Meta Horizon Developer Hub, and ship a separate Quest artifact - from your Play or Fire OS artifacts. + Horizon OS is Meta's operating system for Quest headsets, and OpenIAP + treats it as an Android build target. Select the horizon{' '} + platform flavor, provide the Horizon app id from Meta Horizon Developer + Hub, and ship a Quest artifact that is separate from your Google Play or{' '} + Amazon Fire OS artifacts.

        @@ -50,8 +51,10 @@ function HorizonStoreSetup() {
        @@ -65,7 +68,10 @@ function HorizonStoreSetup() { - +
        Expo uses android.horizon.appId. Bare React Native - and Flutter examples below use a Gradle property named{' '} - horizonAppId and write Android manifest meta-data{' '} + reads a Gradle property named horizonAppId; Flutter + reads HORIZON_APP_ID from{' '} + android/local.properties. Both write Android + manifest meta-data{' '} com.meta.horizon.platform.HORIZON_APP_ID.
        Verification credentialsYour backend or IAPKit project configuration. + Your backend or IAPKit{' '} + project configuration. + Runtime verification payloads use horizon.sku,{' '} horizon.userId, and{' '} @@ -81,21 +87,39 @@ function HorizonStoreSetup() { Store Prerequisites -
          +

          + Complete these once per app in Meta Horizon Developer Hub before + configuring any framework: +

          +
          1. Register the app in Meta Horizon Developer Hub.
          2. -
          3. Configure products and subscriptions with stable SKUs.
          4. - Keep the Horizon app id available to the Android manifest at build + Create your products and subscriptions with stable SKUs — the same + strings you will pass to fetchProducts and{' '} + requestPurchase. +
          5. +
          6. + Copy the Horizon app id so the Android manifest can read it at build time.
          7. -
          8. Test on a Meta Quest device signed into an entitled account.
          9. -
        +
      • + Prepare a Meta Quest device signed into an account that has access + to the app, such as a release-channel member or registered test + user. +
      • +
        Framework Setup +

        + Every framework except Godot ships Quest support through the same + Android horizon flavor; only the switch location differs. + Find your framework here, then follow the matching section below for + full snippets. +

        @@ -156,7 +180,10 @@ function HorizonStoreSetup() { - + @@ -216,9 +243,12 @@ android { React Native

        - react-native-iap has no Expo config plugin. Select the + react-native-iap has no Expo config plugin, so select the Horizon flavor in the app Gradle build and write the app id in the - manifest yourself: + manifest yourself. fireOsEnabled is the switch for{' '} + Amazon Fire OS builds; the + two flavors are mutually exclusive, so keep it false for + Quest artifacts:

        {`# android/gradle.properties horizonEnabled=true @@ -252,11 +282,15 @@ android {

        Flutter uses the same Gradle property model as bare React Native. The app module maps the property into the plugin flavor and injects the - app id through a manifest placeholder: + app id through a manifest placeholder; localProperties is + the loader the Flutter Android template already defines in{' '} + android/app/build.gradle:

        {`# android/gradle.properties horizonEnabled=true fireOsEnabled=false`} + {`# android/local.properties +HORIZON_APP_ID=YOUR_HORIZON_APP_ID`} {`def horizonEnabled = project.findProperty('horizonEnabled')?.toBoolean() ?: false def fireOsEnabled = project.findProperty('fireOsEnabled')?.toBoolean() ?: false def flavor = fireOsEnabled ? 'amazon' : (horizonEnabled ? 'horizon' : 'play') @@ -269,6 +303,10 @@ android { ] } }`} +

        Reference the placeholder in the Android manifest:

        + {``}
        @@ -276,9 +314,11 @@ android { KMP and MAUI

        - KMP publishes a dedicated Android horizonRelease variant. - Build that variant for Quest distribution and keep the app id in the - Android host manifest. + KMP publishes per-store Android variants of the library; Quest apps + consume the horizonRelease variant and keep the app id in + the Android host app's manifest, exactly as in the Native Android + section above. When building the library from source, assemble the + variant directly:

        {`./gradlew :library:assembleHorizonRelease`}

        MAUI selects the Horizon AAR flavor with an MSBuild property:

        @@ -290,10 +330,24 @@ android { Verification

        - Client purchase calls stay the same. For server validation, pass the - Horizon receipt context to{' '} - Validation or IAPKit using - the horizon verification payload. + Client purchase calls stay the same on Quest —{' '} + fetchProducts and requestPurchase work + unchanged. For server validation, pass the Horizon receipt context to{' '} + Validation or to{' '} + IAPKit, OpenIAP's hosted + verification backend, using the horizon payload: +

        + {`await verifyPurchase({ + horizon: { + sku: purchase.productId, + userId: metaUserId, + accessToken: horizonAccessToken, // Meta app credential + }, +});`} +

        + The access token is a Meta app credential. Keep Horizon verification + on trusted infrastructure — your backend or IAPKit — rather than + embedding the token in the shipped app.

        diff --git a/packages/docs/src/pages/docs/setup/store/index.tsx b/packages/docs/src/pages/docs/setup/store/index.tsx index a8ff097ef..6cabed197 100644 --- a/packages/docs/src/pages/docs/setup/store/index.tsx +++ b/packages/docs/src/pages/docs/setup/store/index.tsx @@ -16,11 +16,15 @@ function StoreSetup() { />

        Store Setup

        - Start with your framework setup, then add the store target your release - artifact needs. Store setup is intentionally separate from feature - usage: purchase APIs stay consistent, while build-time and runtime store - selection differ by framework. Each store guide starts with the exact - values you need, where to get them, and where each framework reads them. + Apple App Store and Google Play need no extra store configuration — your{' '} + framework setup guide covers them. This + section is for additional store targets: Meta Quest (Horizon OS), the + Amazon Appstore (Fire OS and Vega OS), and the Onside iOS alternative + marketplace. Finish your framework setup first, then add the store + target your release artifact needs. The purchase APIs stay identical on + every store; only build-time and runtime store selection differ. Each + store guide lists the exact values you need, where to get them, and + where each framework reads them.

        @@ -31,7 +35,7 @@ function StoreSetup() {
        - + @@ -39,7 +43,8 @@ function StoreSetup() { - + @@ -82,25 +95,22 @@ function StoreSetup() { Framework Model

        - Expo can write native configuration during prebuild. Bare React - Native, Flutter, KMP, MAUI, Godot, and native Android projects own - their native build files directly. + Every store guide shows two setup paths, so know which one your + project uses. Expo projects declare store values in the Expo config, + and a config plugin writes the native files during{' '} + expo prebuild. Bare React Native, Flutter, KMP, MAUI, + Godot, and native Android projects edit their native build files + directly.

        • - Use Horizon OS Setup for - Meta Quest app ids, Android flavor selection, and framework-specific - build configuration. -
        • -
        • - Use Amazon Store Setup{' '} - for Fire OS Android artifacts and Vega OS runtime targets. Fire OS - and Vega OS are separate artifacts even when both use Amazon receipt - verification. + Fire OS and Vega OS are separate release artifacts even though both + use Amazon receipt verification — do not reuse one build for the + other.
        • - Use Onside Setup for - Expo-only iOS alternative marketplace support. + Onside is currently expo-iap only; other frameworks + have no Onside build option.
        diff --git a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx index ba8cdad69..7d059f989 100644 --- a/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import LanguageTabs from '../../../../components/LanguageTabs'; import SEO from '../../../../components/SEO'; @@ -167,22 +168,14 @@ function AppTransactionIos() {
        GodotNo dedicated Horizon flavor switch yet. + No dedicated Horizon flavor switch yet, so there is no Godot + section below. + Not applicable.
        TargetSelection modelHow it is selected Setup guide
        Horizon OS - Android horizon flavor for Meta Quest builds. + Build the Android Gradle horizon product flavor for + Meta Quest devices. Horizon OS Setup @@ -48,27 +53,35 @@ function StoreSetup() {
        Amazon Fire OS - Android amazon flavor for Amazon Appstore builds. + Android amazon flavor for Amazon Appstore builds + (experimental, RC packages). - Amazon Store Setup + + Amazon Store Setup — Fire OS +
        Amazon Vega OS - Kepler runtime adapter for React Native for Vega and compatible - Expo Vega targets. + Vega devices run apps on Amazon's Kepler JavaScript runtime + instead of Android; react-native-iap and{' '} + expo-iap switch to their Kepler adapter at runtime + (experimental, RC packages). - Amazon Store Setup + Amazon Store Setup — Vega OS
        OnsideiOS marketplace runtime selected by expo-iap. + iOS alternative marketplace; expo-iap selects the + Onside runtime automatically (Expo only). + Onside Setup
        -
        - Xcode 27 boundary: StoreKit also exposes{' '} - AppTransaction.all, an async sequence of app-acquisition - records. OpenIAP 3 does not export that sequence.{' '} - getAppTransactionIOS returns the current verified app - transaction, while getAllTransactionsIOS remains in-app - purchase history; the two histories are not interchangeable. -
        + + StoreKit also exposes AppTransaction.all, an async + sequence of app-acquisition records. OpenIAP 3 does not export that + sequence. getAppTransactionIOS returns the current + verified app transaction, while getAllTransactionsIOS{' '} + remains in-app purchase history; the two histories are not + interchangeable. + Type Definition diff --git a/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx b/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx index 5f22a7cf7..2a5108db2 100644 --- a/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx +++ b/packages/docs/src/pages/docs/types/ios/subscription-billing-plan-ios.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../../components/AnchorLink'; +import Callout from '../../../../components/Callout'; import CodeBlock from '../../../../components/CodeBlock'; import SEO from '../../../../components/SEO'; import { useScrollToHash } from '../../../../hooks/useScrollToHash'; @@ -27,15 +28,13 @@ function SubscriptionBillingPlansIOS() { products, and commitment renewal details when StoreKit returns them.

        -
        -

        - Availability: billing plan selection requires iOS, - iPadOS, macOS, tvOS, or visionOS 26.4+ and an app compiled with the - StoreKit billing-plan APIs available in Xcode 26.5+ / Swift 6.3+. On - older runtimes, omit billingPlanType and use the - store's default billing plan. -

        -
        + + Billing plan selection requires iOS, iPadOS, macOS, tvOS, or visionOS + 26.4+ and an app compiled with the StoreKit billing-plan APIs available + in Xcode 26.5+ / Swift 6.3+. On older runtimes, omit{' '} + billingPlanType and use the store's default billing + plan. +
        diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index f9c78f4d4..67996367e 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import PlatformTabs from '../../../components/PlatformTabs'; import SEO from '../../../components/SEO'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; @@ -734,23 +735,14 @@ function Purchase() {
        -
        - Flutter 10: only dataAndroid is - accepted. Custom adapters and fixtures must use the canonical - field. See{' '} + + Only dataAndroid is accepted. Custom adapters and + fixtures must use the canonical field. See{' '} the migration schedule . -
        +
        diff --git a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx index 1b0cd771f..4037e7667 100644 --- a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx +++ b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import SEO from '../../../components/SEO'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; import { IAPKIT_URL, trackIapKitClick } from '../../../lib/config'; @@ -182,17 +183,15 @@ function VerifyPurchaseWithProviderProps() { -
        -

        - Client-visible data only: Product client payloads - can be retrieved by apps and must never contain credentials, signing - keys, or server-authoritative rules. The body is limited to 16 KiB - measured as UTF-8 bytes. Use an openiap-kit_pk_{' '} - publishable key in the app. It is extractable and consumes project - quota, but cannot perform administrative operations. Never embed an{' '} - openiap-kit_sk_ secret key. -

        -
        + + Product client payloads can be retrieved by apps and must never + contain credentials, signing keys, or server-authoritative rules. The + body is limited to 16 KiB measured as UTF-8 bytes. Use an{' '} + openiap-kit_pk_ publishable key in the app. It is + extractable and consumes project quota, but cannot perform + administrative operations. Never embed an openiap-kit_sk_{' '} + secret key. +

        See the{' '} diff --git a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx index 4dedc0e4b..d799cab89 100644 --- a/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx +++ b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import LanguageTabs from '../../../components/LanguageTabs'; import SEO from '../../../components/SEO'; @@ -257,15 +258,13 @@ function VerifyPurchaseWithProviderResult() { -

        -

        - Public data: Anyone able to call the project's - client endpoints may receive this payload. Never put secrets, - credentials, signing keys, or private authorization rules in it. - Entitlement decisions must continue to use isValid,{' '} - state, and the store-verified productId. -

        -
        + + Anyone able to call the project's client endpoints may receive this + payload. Never put secrets, credentials, signing keys, or private + authorization rules in it. Entitlement decisions must continue to use{' '} + isValid, state, and the store-verified{' '} + productId. +

        The field is omitted by default and for invalid receipts, absent or diff --git a/packages/docs/src/pages/docs/updates/announcements.tsx b/packages/docs/src/pages/docs/updates/announcements.tsx index 5aaa7b1f2..b7a1e8e92 100644 --- a/packages/docs/src/pages/docs/updates/announcements.tsx +++ b/packages/docs/src/pages/docs/updates/announcements.tsx @@ -1,5 +1,6 @@ import { useMemo } from 'react'; import { Link } from 'react-router-dom'; +import Callout from '../../../components/Callout'; import SEO from '../../../components/SEO'; import { useScrollToHash, getHashId } from '../../../hooks/useScrollToHash'; import Pagination from '../../../components/Pagination'; @@ -36,14 +37,6 @@ const linkIconStyle = { fontSize: '1.2rem', }; -const calloutStyle = { - marginTop: '1.5rem', - padding: '1rem', - background: 'var(--bg-secondary)', - borderRadius: '0.5rem', - borderLeft: '4px solid var(--primary-color)', -}; - interface Announcement { id: string; date: Date; @@ -159,7 +152,7 @@ function Announcements() {

      -

      + See the{' '} complete OpenIAP 3 release notes @@ -170,7 +163,7 @@ function Announcements() { Deprecations & 3.0 Migration catalog . -

      + ), }, @@ -235,14 +228,14 @@ function Announcements() { input fallback for the remainder of Flutter 9.x; the alias is not a public Purchase field and is removed in 10.0.0.

      -

      + See the complete{' '} deprecation schedule and migration catalog . Each package reaches its major independently, so this notice does not promise a shared release date. -

      + ), }, @@ -388,8 +381,8 @@ function Announcements() { cross-platform compatibility. Our core libraries remain MIT licensed and free to use.

      -
      - Get started: Read the{' '} + + Read the{' '} {' '} for React Native for Vega apps and compatible Expo projects. -
      + ), }, @@ -484,9 +477,8 @@ function Announcements() { }} /> -
      - Getting Started: Install OpenIap.Maui{' '} - from NuGet or open the{' '} + + Install OpenIap.Maui from NuGet or open the{' '} . See the .NET MAUI setup guide for full documentation. -
      + ), }, @@ -704,13 +696,13 @@ function Announcements() {

    -
    - For existing users: There are no breaking changes. - The major version bump reflects the transition to the monorepo as - the new home for development and releases — not API changes. Package - names and installation commands remain the same. Just update to the - new version and you're good to go. -
    + + There are no breaking changes. The major version bump reflects the + transition to the monorepo as the new home for development and + releases — not API changes. Package names and installation commands + remain the same. Just update to the new version and you're good to + go. + ), }, @@ -798,9 +790,8 @@ function Announcements() { }} /> -
    - Getting Started: Download GDScript type definitions - from the{' '} + + Download GDScript type definitions from the{' '} Types page {' '} @@ -814,7 +805,7 @@ function Announcements() { godot-iap repository {' '} for full documentation. -
    + ), }, @@ -908,9 +899,8 @@ function Announcements() { }} /> -
    - Getting Started: Use the new{' '} - verifyPurchaseWithProvider API with{' '} + + Use the new verifyPurchaseWithProvider API with{' '} provider: 'iapkit'. See the{' '} {' '} for details. -
    + ), }, @@ -1008,14 +998,14 @@ function Announcements() { }} /> -
    - Getting Started: Available in{' '} - openiap-google@1.3.0 and later. Check out the{' '} + + Available in openiap-google@1.3.0 and later. Check out + the{' '} Horizon OS guide {' '} for details. -
    + ), }, @@ -1164,10 +1154,10 @@ function Announcements() { -

    - Next: We will be publishing quickstart guides and - API references within the Docs → Modules section. -

    + + We will be publishing quickstart guides and API references within + the Docs → Modules section. + ), }, @@ -1213,12 +1203,11 @@ function Announcements() { Meta! This partnership marks a significant milestone in our mission to standardize and simplify in-app purchases across all platforms.

    -

    - Note: OpenIAP will continue to operate - independently with the same commitment to developer experience and - cross-platform compatibility. Our core libraries remain MIT licensed - and free to use. -

    + + OpenIAP will continue to operate independently with the same + commitment to developer experience and cross-platform compatibility. + Our core libraries remain MIT licensed and free to use. + ), }, diff --git a/packages/docs/src/pages/docs/updates/deprecations.tsx b/packages/docs/src/pages/docs/updates/deprecations.tsx index 425d8556b..facb13610 100644 --- a/packages/docs/src/pages/docs/updates/deprecations.tsx +++ b/packages/docs/src/pages/docs/updates/deprecations.tsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; +import Callout from '../../../components/Callout'; import SEO from '../../../components/SEO'; import { LIBRARIES } from '../../../lib/images'; @@ -477,14 +478,6 @@ const packageCompatibilityMigrations = [ }, ] as const; -const calloutStyle = { - padding: '1rem', - background: 'rgba(220, 104, 67, 0.1)', - borderLeft: '4px solid var(--accent-color)', - borderRadius: '0.5rem', - margin: '1rem 0', -}; - function Deprecations() { return (
    @@ -502,12 +495,11 @@ function Deprecations() { before upgrading.

    -
    - Breaking major release: the listed versions do not - include compatibility wrappers, deprecated schema members, or legacy - custom-wire aliases. Upgrade coordinated native and framework - dependencies together. -
    + + The listed versions do not include compatibility wrappers, deprecated + schema members, or legacy custom-wire aliases. Upgrade coordinated + native and framework dependencies together. +
    diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 2b274b9b6..6572c3aaa 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -2,6 +2,7 @@ import { Link } from 'react-router-dom'; import { useMemo } from 'react'; import SEO from '../../../components/SEO'; import { useScrollToHash, getHashId } from '../../../hooks/useScrollToHash'; +import Callout from '../../../components/Callout'; import CodeBlock from '../../../components/CodeBlock'; import Pagination from '../../../components/Pagination'; import AnchorLink from '../../../components/AnchorLink'; @@ -1054,16 +1055,7 @@ function Releases() { library.

    -
    -
    Package Releases
    +
      @@ -1079,7 +1071,7 @@ function Releases() { ))}
    -
    +
    ), }, diff --git a/packages/docs/src/pages/docs/webhooks.tsx b/packages/docs/src/pages/docs/webhooks.tsx index f483332aa..78f045539 100644 --- a/packages/docs/src/pages/docs/webhooks.tsx +++ b/packages/docs/src/pages/docs/webhooks.tsx @@ -1,4 +1,5 @@ import AnchorLink from '../../components/AnchorLink'; +import Callout from '../../components/Callout'; import SEO from '../../components/SEO'; import { useScrollToHash } from '../../hooks/useScrollToHash'; @@ -23,16 +24,14 @@ function Webhooks() {
             Apple / Google → IAPKit
           
    -
    -

    - IAPKit does not stream webhook events to mobile SDKs. There is no - outbound SSE, WebSocket, push, or long-poll API. Apps should verify - purchases and refresh status or entitlements through the bounded - request/response APIs. If your own backend protects paid resources, - that backend remains responsible for its entitlement decision and any - app push notification. -

    -
    + + IAPKit does not stream webhook events to mobile SDKs. There is no + outbound SSE, WebSocket, push, or long-poll API. Apps should verify + purchases and refresh status or entitlements through the bounded + request/response APIs. If your own backend protects paid resources, that + backend remains responsible for its entitlement decision and any app + push notification. +
    diff --git a/packages/docs/src/styles/components.css b/packages/docs/src/styles/components.css index 90e5ddb0d..567ec457a 100644 --- a/packages/docs/src/styles/components.css +++ b/packages/docs/src/styles/components.css @@ -292,45 +292,75 @@ color: var(--primary-dark); } -/* Alert Cards */ -.alert-card { - border-radius: 6px; - padding: var(--spacing-md); +/* Callouts (semantic asides — see Callout.tsx) */ +.callout { + border-left: 3px solid var(--callout-color); + border-radius: 0 6px 6px 0; + background: var(--callout-bg); + padding: 0.75rem 1rem; margin: var(--spacing-md) 0; font-size: var(--font-size-sm); } -/* Reduce spacing when alert-card follows TL;DR box */ -.doc-page .tldr-box + .alert-card { +.callout:has(+ section) { + margin-bottom: 0; +} + +/* Reduce spacing when a callout follows the TL;DR box */ +.doc-page .tldr-box + .callout { margin-top: 0 !important; } -/* Reduce bottom margin when alert-card is followed by section */ -.alert-card:has(+ section) { - margin-bottom: 0; +.callout-label { + margin: 0 0 var(--spacing-xs); + font-size: var(--font-size-xs); + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--callout-color); } -.doc-page .alert-card p { +.doc-page .callout p { margin: 0; + font-size: var(--font-size-sm); +} + +/* Beats .doc-page .callout p (0,2,1) with three classes (0,3,0) */ +.doc-page .callout .callout-label { + margin: 0 0 var(--spacing-xs); + font-size: var(--font-size-xs); + color: var(--callout-color); } -.alert-card ul, -.alert-card ol { - margin-top: 0.5rem; +.callout-body p + p, +.doc-page .callout-body p + p { + margin-top: var(--spacing-sm); +} + +.callout-body ul, +.callout-body ol, +.doc-page .callout-body ul, +.doc-page .callout-body ol { + margin-top: var(--spacing-sm); margin-bottom: 0; } -.alert-card--info { - background: rgba(59, 130, 246, 0.1); - border: 1px solid rgba(59, 130, 246, 0.3); +.callout--note { + --callout-color: var(--primary-dark); + --callout-bg: rgba(164, 116, 101, 0.08); +} + +.callout--tip { + --callout-color: #4e7d5b; + --callout-bg: rgba(78, 125, 91, 0.08); } -.alert-card--warning { - background: rgba(255, 200, 0, 0.1); - border: 1px solid rgba(255, 200, 0, 0.3); +.callout--important { + --callout-color: #c05531; + --callout-bg: rgba(220, 104, 67, 0.09); } -.alert-card--success { - background: rgba(16, 185, 129, 0.1); - border: 1px solid rgba(16, 185, 129, 0.3); +.callout--warning { + --callout-color: #b3403a; + --callout-bg: rgba(179, 64, 58, 0.08); } diff --git a/packages/docs/src/styles/dark-mode.css b/packages/docs/src/styles/dark-mode.css index 068a6ef80..226e47739 100644 --- a/packages/docs/src/styles/dark-mode.css +++ b/packages/docs/src/styles/dark-mode.css @@ -159,21 +159,27 @@ filter: invert(1) brightness(1.5) contrast(1.1); } -/* Alert Cards - Dark Mode */ -:root.dark .alert-card--info { - background: rgba(59, 130, 246, 0.25); - border: 2px solid rgba(59, 130, 246, 0.7); +/* Callouts - Dark Mode */ +:root.dark .callout { color: var(--text-primary); } -:root.dark .alert-card--warning { - background: rgba(255, 180, 0, 0.2); - border: 2px solid rgba(255, 180, 0, 0.7); - color: var(--text-primary); +:root.dark .callout--note { + --callout-color: #c5a99e; + --callout-bg: rgba(164, 116, 101, 0.16); } -:root.dark .alert-card--success { - background: rgba(16, 185, 129, 0.25); - border: 2px solid rgba(16, 185, 129, 0.7); - color: var(--text-primary); +:root.dark .callout--tip { + --callout-color: #8fbf9c; + --callout-bg: rgba(78, 125, 91, 0.18); +} + +:root.dark .callout--important { + --callout-color: #e8926e; + --callout-bg: rgba(220, 104, 67, 0.16); +} + +:root.dark .callout--warning { + --callout-color: #e2837d; + --callout-bg: rgba(179, 64, 58, 0.18); } diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css index 04f811f8c..08cda162f 100644 --- a/packages/docs/src/styles/documentation.css +++ b/packages/docs/src/styles/documentation.css @@ -793,10 +793,20 @@ } /* Documentation Links - Oatmeal Colors */ +.doc-page { + /* Persistent link underline so links are visible without hovering — + inline code shares the link color, so this is the only cue */ + --doc-link-underline: rgba(139, 101, 69, 0.35); +} + +:root.dark .doc-page { + --doc-link-underline: rgba(212, 165, 116, 0.4); +} + .doc-page a:not(.anchor-link, .btn) { color: #8b6545; /* Warm brown */ text-decoration: none; - border-bottom: 1px solid transparent; + border-bottom: 1px solid var(--doc-link-underline); transition: border-color 0.2s ease; } diff --git a/scripts/audit-deprecation-schedule.mjs b/scripts/audit-deprecation-schedule.mjs index 455e610b0..a78b2a458 100644 --- a/scripts/audit-deprecation-schedule.mjs +++ b/scripts/audit-deprecation-schedule.mjs @@ -328,7 +328,7 @@ const requiredTexts = [ { file: "packages/docs/src/pages/docs/updates/deprecations.tsx", values: [ - "Breaking major release:", + 'title="Breaking major release"', "Last compatible major", "Removed in", "react-native-iap 16.0.0", diff --git a/scripts/audit-non-godot-parity.mjs b/scripts/audit-non-godot-parity.mjs index fe76c6a91..679913851 100644 --- a/scripts/audit-non-godot-parity.mjs +++ b/scripts/audit-non-godot-parity.mjs @@ -6214,7 +6214,7 @@ function checkFrameworkDependencyHygiene() { "packages/docs/src/pages/docs/setup/maui.tsx", [ ".NET 10 SDK", - "Google Billing, Play", + "Google Play Billing, Play Services", "net10.0-ios;net10.0-android;net10.0-maccatalyst", "OpenIap.Maui 2.x", "supports .NET 10 only",