diff --git a/packages/docs/src/components/MenuDropdown.tsx b/packages/docs/src/components/MenuDropdown.tsx index 9403c4eb8..9337c1396 100644 --- a/packages/docs/src/components/MenuDropdown.tsx +++ b/packages/docs/src/components/MenuDropdown.tsx @@ -1,18 +1,132 @@ -import { useState, useRef, useEffect } from 'react'; +import { useId, useState, useEffect } from 'react'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; -interface MenuItem { +export interface MenuItem { to: string; label: string; } +export interface MenuGroup { + label: string; + items: MenuItem[]; +} + +export type MenuEntry = MenuItem | MenuGroup; + +function isGroup(entry: MenuEntry): entry is MenuGroup { + return 'items' in entry && Array.isArray((entry as MenuGroup).items); +} + interface MenuDropdownProps { title: string; titleTo: string; - items: MenuItem[]; + items: MenuEntry[]; onItemClick?: () => void; } +interface SubMenuProps { + group: MenuGroup; + onItemClick?: () => void; + parentExpanded: boolean; +} + +function Chevron({ isExpanded }: { isExpanded: boolean }) { + return ( + + ); +} + +function SubMenu({ group, onItemClick, parentExpanded }: SubMenuProps) { + const [isExpanded, setIsExpanded] = useState(false); + const location = useLocation(); + const submenuContentId = useId(); + // NavLinks should leave the tab order whenever this submenu OR its + // parent dropdown is collapsed — otherwise keyboard users can tab into + // hidden links. + const navLinkTabIndex = parentExpanded && isExpanded ? 0 : -1; + + const isAnyChildActive = group.items.some( + (item) => location.pathname === item.to + ); + + useEffect(() => { + if (isAnyChildActive) { + setIsExpanded(true); + } + }, [isAnyChildActive]); + + const toggleExpanded = () => setIsExpanded((v) => !v); + + return ( +
  • + {/* Single disclosure button — both halves do the same thing, so + collapsing into one element keeps the keyboard tab order at one + stop per group instead of two redundant ones. */} + +
    + +
    +
  • + ); +} + export function MenuDropdown({ title, titleTo, @@ -20,39 +134,35 @@ export function MenuDropdown({ onItemClick, }: MenuDropdownProps) { const [isExpanded, setIsExpanded] = useState(false); - const [isHovered, setIsHovered] = useState(false); - const contentRef = useRef(null); - const [height, setHeight] = useState(0); const location = useLocation(); const navigate = useNavigate(); + const contentId = useId(); const isTitleActive = location.pathname === titleTo; - const isChildActive = items.some((item) => location.pathname === item.to); + const isChildActive = items.some((entry) => + isGroup(entry) + ? entry.items.some((i) => location.pathname === i.to) + : location.pathname === entry.to + ); const isGroupActive = isTitleActive || isChildActive; useEffect(() => { - if (contentRef.current) { - setHeight(isExpanded ? contentRef.current.scrollHeight : 0); - } - }, [isExpanded]); - - useEffect(() => { - // Auto-expand when navigating to the title page or any child if (isGroupActive) { setIsExpanded(true); } }, [isGroupActive]); + // Title click: always navigate + close the mobile drawer. Collapsing + // is handled exclusively by the dedicated chevron toggle so screen- + // reader semantics stay clean (the title is a nav control, not a + // disclosure control). const handleTitleClick = () => { - // Always navigate and expand — never collapse from title click setIsExpanded(true); navigate(titleTo); onItemClick?.(); }; - const toggleExpanded = () => { - setIsExpanded(!isExpanded); - }; + const toggleExpanded = () => setIsExpanded((v) => !v); return (
  • @@ -60,50 +170,59 @@ export function MenuDropdown({ className={`menu-dropdown-header ${isTitleActive ? 'active' : isChildActive ? 'group-active' : ''}`} >
      - {items.map((item) => ( -
    • - - `menu-dropdown-item ${isActive ? 'active' : ''}` - } - onClick={onItemClick} - > - └ - {item.label} - -
    • - ))} + {items.map((entry) => + isGroup(entry) ? ( + + ) : ( +
    • + + `menu-dropdown-item ${isActive ? 'active' : ''}` + } + onClick={onItemClick} + > + + {entry.label} + +
    • + ) + )}
  • diff --git a/packages/docs/src/lib/searchData.ts b/packages/docs/src/lib/searchData.ts index ecabe8565..1ec9bdc08 100644 --- a/packages/docs/src/lib/searchData.ts +++ b/packages/docs/src/lib/searchData.ts @@ -17,7 +17,7 @@ export const apiData: ApiItem[] = [ description: 'Initialize connection to the store service', parameters: '', returns: 'Boolean!', - path: '/docs/apis/connection#init-connection', + path: '/docs/apis/init-connection', }, { id: 'end-connection', @@ -26,7 +26,7 @@ export const apiData: ApiItem[] = [ description: 'End connection to the store service', parameters: '', returns: 'Boolean!', - path: '/docs/apis/connection#end-connection', + path: '/docs/apis/end-connection', }, // Product Management @@ -37,7 +37,7 @@ export const apiData: ApiItem[] = [ description: 'Retrieve products or subscriptions from the store', parameters: 'ProductRequest', returns: '[Product!]!', - path: '/docs/apis/products#fetch-products', + path: '/docs/apis/fetch-products', }, { id: 'get-available-purchases', @@ -46,7 +46,7 @@ export const apiData: ApiItem[] = [ description: 'Get all available purchases for the current user', parameters: 'PurchaseOptions?', returns: '[Purchase!]!', - path: '/docs/apis/products#get-available-purchases', + path: '/docs/apis/get-available-purchases', }, // Purchase Operations @@ -57,7 +57,7 @@ export const apiData: ApiItem[] = [ description: 'Request a purchase (one-time or subscription)', parameters: 'RequestPurchaseProps', returns: 'Purchase!', - path: '/docs/apis/purchase#request-purchase', + path: '/docs/apis/request-purchase', }, { id: 'finish-transaction', @@ -67,7 +67,7 @@ export const apiData: ApiItem[] = [ 'Complete a purchase transaction. Must be called after successful verification', parameters: 'Purchase!, isConsumable: Boolean?', returns: 'Void', - path: '/docs/apis/purchase#finish-transaction', + path: '/docs/apis/finish-transaction', }, { id: 'restore-purchases', @@ -76,7 +76,7 @@ export const apiData: ApiItem[] = [ description: 'Restore completed transactions (cross-platform)', parameters: '', returns: 'Void', - path: '/docs/apis/purchase#restore-purchases', + path: '/docs/apis/restore-purchases', }, { id: 'get-storefront', @@ -85,7 +85,7 @@ export const apiData: ApiItem[] = [ description: 'Get storefront country code for the active user', parameters: '', returns: 'String!', - path: '/docs/apis/purchase#get-storefront', + path: '/docs/apis/get-storefront', }, // Subscription Management @@ -96,7 +96,7 @@ export const apiData: ApiItem[] = [ description: 'Get all active subscriptions with detailed information', parameters: 'subscriptionIds: [String]?', returns: '[ActiveSubscription!]!', - path: '/docs/apis/subscription#get-active-subscriptions', + path: '/docs/apis/get-active-subscriptions', }, { id: 'has-active-subscriptions', @@ -105,7 +105,7 @@ export const apiData: ApiItem[] = [ description: 'Check if the user has any active subscriptions', parameters: 'subscriptionIds: [String]?', returns: 'Boolean!', - path: '/docs/apis/subscription#has-active-subscriptions', + path: '/docs/apis/has-active-subscriptions', }, { id: 'deep-link-to-subscriptions', @@ -114,7 +114,7 @@ export const apiData: ApiItem[] = [ description: 'Open native subscription management interface', parameters: 'DeepLinkOptions', returns: 'Void', - path: '/docs/apis/subscription#deep-link-to-subscriptions', + path: '/docs/apis/deep-link-to-subscriptions', }, // Verification @@ -125,7 +125,7 @@ export const apiData: ApiItem[] = [ description: 'Verify purchases with your server or platform providers', parameters: 'PurchaseVerificationProps!', returns: 'PurchaseVerificationResult!', - path: '/docs/apis/validation#verify-purchase', + path: '/docs/features/validation#verify-purchase', }, { id: 'verify-purchase-with-provider', @@ -134,7 +134,17 @@ export const apiData: ApiItem[] = [ description: 'Verify purchases using IAPKit or other providers', parameters: 'VerifyPurchaseWithProviderProps!', returns: 'VerifyPurchaseWithProviderResult!', - path: '/docs/apis/validation#verify-purchase-with-provider', + path: '/docs/features/validation#verify-purchase-with-provider', + }, + { + id: 'validate-receipt', + title: 'validateReceipt', + category: 'Validation', + description: + 'Deprecated. Use verifyPurchase instead. Cross-platform receipt validation entry point.', + parameters: 'options: ReceiptValidationProps!', + returns: 'ReceiptValidationResult!', + path: '/docs/apis/validate-receipt', }, // iOS Specific @@ -145,7 +155,7 @@ export const apiData: ApiItem[] = [ description: 'Clear pending transactions', parameters: '', returns: 'Boolean!', - path: '/docs/apis/ios#clear-transaction-ios', + path: '/docs/apis/ios/clear-transaction-ios', }, { id: 'sync-ios', @@ -154,7 +164,7 @@ export const apiData: ApiItem[] = [ description: 'Force StoreKit transaction sync (iOS 15+)', parameters: '', returns: 'Boolean!', - path: '/docs/apis/ios#sync-ios', + path: '/docs/apis/ios/sync-ios', }, { id: 'get-promoted-product-ios', @@ -163,7 +173,7 @@ export const apiData: ApiItem[] = [ description: 'Get the currently promoted product (iOS 11+)', parameters: '', returns: 'ProductIOS', - path: '/docs/apis/ios#get-promoted-product-ios', + path: '/docs/apis/ios/get-promoted-product-ios', }, { id: 'request-purchase-on-promoted-product-ios', @@ -172,7 +182,7 @@ export const apiData: ApiItem[] = [ description: 'Purchase a promoted product (iOS 11+)', parameters: '', returns: 'Boolean!', - path: '/docs/apis/ios#request-purchase-on-promoted-product-ios', + path: '/docs/apis/ios/request-purchase-on-promoted-product-ios', }, { id: 'get-pending-transactions-ios', @@ -181,7 +191,7 @@ export const apiData: ApiItem[] = [ description: 'Retrieve pending StoreKit transactions', parameters: '', returns: '[PurchaseIOS!]!', - path: '/docs/apis/ios#get-pending-transactions-ios', + path: '/docs/apis/ios/get-pending-transactions-ios', }, { id: 'is-eligible-for-intro-offer-ios', @@ -190,7 +200,7 @@ export const apiData: ApiItem[] = [ description: 'Check introductory offer eligibility', parameters: 'groupID: String!', returns: 'Boolean!', - path: '/docs/apis/ios#is-eligible-for-intro-offer-ios', + path: '/docs/apis/ios/is-eligible-for-intro-offer-ios', }, { id: 'subscription-status-ios', @@ -199,7 +209,7 @@ export const apiData: ApiItem[] = [ description: 'Get StoreKit 2 subscription status (iOS 15+)', parameters: 'sku: String!', returns: '[SubscriptionStatusIOS!]!', - path: '/docs/apis/ios#subscription-status-ios', + path: '/docs/apis/ios/subscription-status-ios', }, { id: 'current-entitlement-ios', @@ -208,7 +218,7 @@ export const apiData: ApiItem[] = [ description: 'Get current StoreKit 2 entitlement (iOS 15+)', parameters: 'sku: String!', returns: 'PurchaseIOS', - path: '/docs/apis/ios#current-entitlement-ios', + path: '/docs/apis/ios/current-entitlement-ios', }, { id: 'latest-transaction-ios', @@ -217,7 +227,7 @@ export const apiData: ApiItem[] = [ description: 'Get latest StoreKit 2 transaction (iOS 15+)', parameters: 'sku: String!', returns: 'PurchaseIOS', - path: '/docs/apis/ios#latest-transaction-ios', + path: '/docs/apis/ios/latest-transaction-ios', }, { id: 'show-manage-subscriptions-ios', @@ -226,7 +236,7 @@ export const apiData: ApiItem[] = [ description: 'Open subscription management UI and return changes (iOS 15+)', parameters: '', returns: '[PurchaseIOS!]!', - path: '/docs/apis/ios#show-manage-subscriptions-ios', + path: '/docs/apis/ios/show-manage-subscriptions-ios', }, { id: 'begin-refund-request-ios', @@ -235,7 +245,7 @@ export const apiData: ApiItem[] = [ description: 'Initiate refund request (iOS 15+)', parameters: 'sku: String!', returns: 'String', - path: '/docs/apis/ios#begin-refund-request-ios', + path: '/docs/apis/ios/begin-refund-request-ios', }, { id: 'is-transaction-verified-ios', @@ -244,7 +254,7 @@ export const apiData: ApiItem[] = [ description: 'Verify StoreKit 2 transaction signature', parameters: 'sku: String!', returns: 'Boolean!', - path: '/docs/apis/ios#is-transaction-verified-ios', + path: '/docs/apis/ios/is-transaction-verified-ios', }, { id: 'get-transaction-jws-ios', @@ -253,7 +263,7 @@ export const apiData: ApiItem[] = [ description: 'Get the transaction JWS (StoreKit 2)', parameters: 'sku: String!', returns: 'String', - path: '/docs/apis/ios#get-transaction-jws-ios', + path: '/docs/apis/ios/get-transaction-jws-ios', }, { id: 'get-receipt-data-ios', @@ -262,7 +272,7 @@ export const apiData: ApiItem[] = [ description: 'Get base64-encoded receipt data for validation', parameters: '', returns: 'String', - path: '/docs/apis/ios#get-receipt-data-ios', + path: '/docs/apis/ios/get-receipt-data-ios', }, { id: 'present-code-redemption-sheet-ios', @@ -271,7 +281,7 @@ export const apiData: ApiItem[] = [ description: 'Present the App Store code redemption sheet', parameters: '', returns: 'Boolean!', - path: '/docs/apis/ios#present-code-redemption-sheet-ios', + path: '/docs/apis/ios/present-code-redemption-sheet-ios', }, { id: 'get-app-transaction-ios', @@ -279,8 +289,8 @@ export const apiData: ApiItem[] = [ category: 'iOS Specific', description: 'Fetch the current app transaction (iOS 16+)', parameters: '', - returns: 'AppTransaction', - path: '/docs/apis/ios#get-app-transaction-ios', + returns: 'AppTransactionIOS', + path: '/docs/apis/ios/get-app-transaction-ios', }, { id: 'external-purchase-ios', @@ -289,7 +299,93 @@ export const apiData: ApiItem[] = [ description: 'External purchase flow for iOS 17.4+', parameters: '', returns: '', - path: '/docs/apis/ios#external-purchase', + path: '/docs/features/external-purchase', + }, + { + id: 'get-all-transactions-ios', + title: 'getAllTransactionsIOS', + category: 'iOS Specific', + description: + 'Get the full StoreKit 2 transaction history as PurchaseIOS values (iOS 18+ requires SK2ConsumableTransactionHistory Info.plist key for consumables)', + parameters: '', + returns: '[PurchaseIOS!]!', + path: '/docs/apis/ios/get-all-transactions-ios', + }, + { + id: 'get-storefront-ios', + title: 'getStorefrontIOS', + category: 'iOS Specific', + description: 'Deprecated. Use getStorefront() (cross-platform) instead.', + parameters: '', + returns: 'String!', + path: '/docs/apis/ios/get-storefront-ios', + }, + { + id: 'validate-receipt-ios', + title: 'validateReceiptIOS', + category: 'iOS Specific', + description: 'Deprecated. Use verifyPurchase instead.', + parameters: 'options: ReceiptValidationProps!', + returns: 'ReceiptValidationResultIOS!', + path: '/docs/apis/ios/validate-receipt-ios', + }, + { + id: 'can-present-external-purchase-notice-ios', + title: 'canPresentExternalPurchaseNoticeIOS', + category: 'iOS Specific', + description: + 'Check if the external purchase notice sheet can be presented (iOS 17.4+)', + parameters: '', + returns: 'Boolean!', + path: '/docs/apis/ios/can-present-external-purchase-notice-ios', + }, + { + id: 'present-external-purchase-notice-sheet-ios', + title: 'presentExternalPurchaseNoticeSheetIOS', + category: 'iOS Specific', + description: "Present Apple's compliance notice sheet (iOS 17.4+)", + parameters: '', + returns: 'ExternalPurchaseNoticeResultIOS!', + path: '/docs/apis/ios/present-external-purchase-notice-sheet-ios', + }, + { + id: 'present-external-purchase-link-ios', + title: 'presentExternalPurchaseLinkIOS', + category: 'iOS Specific', + description: 'Open the external purchase URL in Safari (iOS 18.2+)', + parameters: 'url: String!', + returns: 'ExternalPurchaseLinkResultIOS!', + path: '/docs/apis/ios/present-external-purchase-link-ios', + }, + { + id: 'is-eligible-for-external-purchase-custom-link-ios', + title: 'isEligibleForExternalPurchaseCustomLinkIOS', + category: 'iOS Specific', + description: + 'Check whether the app can use the iOS 18.1+ ExternalPurchaseCustomLink API', + parameters: '', + returns: 'Boolean!', + path: '/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios', + }, + { + id: 'get-external-purchase-custom-link-token-ios', + title: 'getExternalPurchaseCustomLinkTokenIOS', + category: 'iOS Specific', + description: + 'Get the iOS 18.1+ ExternalPurchaseCustomLink token for reporting transactions to Apple', + parameters: '', + returns: 'String', + path: '/docs/apis/ios/get-external-purchase-custom-link-token-ios', + }, + { + id: 'show-external-purchase-custom-link-notice-ios', + title: 'showExternalPurchaseCustomLinkNoticeIOS', + category: 'iOS Specific', + description: + 'Show the iOS 18.1+ ExternalPurchaseCustomLink notice sheet before linking out to external purchases', + parameters: '', + returns: 'Boolean!', + path: '/docs/apis/ios/show-external-purchase-custom-link-notice-ios', }, // Android Specific @@ -300,7 +396,7 @@ export const apiData: ApiItem[] = [ description: 'Acknowledge a non-consumable purchase or subscription', parameters: 'purchaseToken: String!', returns: 'Boolean!', - path: '/docs/apis/android#acknowledge-purchase-android', + path: '/docs/apis/android/acknowledge-purchase-android', }, { id: 'consume-purchase-android', @@ -309,7 +405,7 @@ export const apiData: ApiItem[] = [ description: 'Consume a purchase (for consumable products only)', parameters: 'purchaseToken: String!', returns: 'Boolean!', - path: '/docs/apis/android#consume-purchase-android', + path: '/docs/apis/android/consume-purchase-android', }, { id: 'check-alternative-billing-availability-android', @@ -319,7 +415,7 @@ export const apiData: ApiItem[] = [ 'Check if alternative billing is available (Step 1 of alternative billing)', parameters: '', returns: 'Boolean!', - path: '/docs/apis/android#check-alternative-billing-availability-android', + path: '/docs/apis/android/check-alternative-billing-availability-android', }, { id: 'show-alternative-billing-dialog-android', @@ -329,7 +425,7 @@ export const apiData: ApiItem[] = [ 'Show alternative billing dialog to user (Step 2 of alternative billing)', parameters: '', returns: 'Boolean!', - path: '/docs/apis/android#show-alternative-billing-dialog-android', + path: '/docs/apis/android/show-alternative-billing-dialog-android', }, { id: 'create-alternative-billing-token-android', @@ -339,10 +435,50 @@ export const apiData: ApiItem[] = [ 'Create external transaction token for Google Play (Step 3 of alternative billing)', parameters: '', returns: 'String', - path: '/docs/apis/android#create-alternative-billing-token-android', + path: '/docs/apis/android/create-alternative-billing-token-android', + }, + { + id: 'enable-billing-program-android', + title: 'enableBillingProgramAndroid', + category: 'Android Specific', + description: + 'Step 0 of Billing Programs API. Enable a billing program before initConnection() (Billing Library 8.2.0+)', + parameters: 'config: EnableBillingProgramConfigAndroid!', + returns: 'Boolean!', + path: '/docs/apis/android/enable-billing-program-android', + }, + { + id: 'is-billing-program-available-android', + title: 'isBillingProgramAvailableAndroid', + category: 'Android Specific', + description: + 'Step 1 of Billing Programs API. Check if a billing program is available for the current user', + parameters: 'programId: String!', + returns: 'BillingProgramAvailabilityResultAndroid!', + path: '/docs/apis/android/is-billing-program-available-android', + }, + { + id: 'launch-external-link-android', + title: 'launchExternalLinkAndroid', + category: 'Android Specific', + description: + 'Step 2 of Billing Programs API. Launch external link flow — shows Play Store dialog and optionally launches external URL', + parameters: 'params: LaunchExternalLinkParamsAndroid!', + returns: 'LaunchExternalLinkResultAndroid!', + path: '/docs/apis/android/launch-external-link-android', + }, + { + id: 'create-billing-program-reporting-details-android', + title: 'createBillingProgramReportingDetailsAndroid', + category: 'Android Specific', + description: + 'Step 3 of Billing Programs API. Create reporting details with external transaction token after successful payment', + parameters: '', + returns: 'BillingProgramReportingDetailsAndroid!', + path: '/docs/apis/android/create-billing-program-reporting-details-android', }, - // Debugging & Logging + // Debugging & Logging (moved to Features) { id: 'debugging-logging', title: 'Debugging & Logging', @@ -350,7 +486,7 @@ export const apiData: ApiItem[] = [ description: 'Enable verbose logging for development', parameters: '', returns: '', - path: '/docs/apis/debugging', + path: '/docs/features/debugging', }, { id: 'enable-logging', @@ -359,7 +495,7 @@ export const apiData: ApiItem[] = [ description: 'Enable or disable debug logs', parameters: 'Boolean', returns: '', - path: '/docs/apis/debugging#enable-logging', + path: '/docs/features/debugging#enable-logging', }, { id: 'baseplanid-limitation', @@ -368,7 +504,7 @@ export const apiData: ApiItem[] = [ description: 'Understanding basePlanId limitations with multiple offers', parameters: '', returns: '', - path: '/docs/apis/debugging#android-baseplanid-limitation', + path: '/docs/features/debugging#android-baseplanid-limitation', }, // Documentation Pages @@ -380,6 +516,69 @@ export const apiData: ApiItem[] = [ 'External purchase links for iOS - redirect users to external payment websites (iOS 16.0+)', path: '/docs/features/external-purchase', }, + { + id: 'refund-page', + title: 'Refund', + category: 'Documentation', + description: + 'Handle refunds across iOS and Android. beginRefundRequestIOS, Android auto-refund, App Store Server Notifications, Real-time Developer Notifications', + path: '/docs/features/refund', + }, + { + id: 'refund-ios', + title: 'iOS Refund Request', + category: 'Refund', + description: + 'Present in-app refund sheet on iOS 15+ via beginRefundRequestIOS', + path: '/docs/features/refund#begin-refund-request-ios', + }, + { + id: 'refund-ios-server-notifications', + title: 'App Store Server Notifications V2 (Refund)', + category: 'Refund', + description: + 'Detect approved iOS refunds via REFUND, REVOKE, REFUND_DECLINED notifications', + path: '/docs/features/refund#ios-server-notifications', + }, + { + id: 'refund-android-rtdn', + title: 'Real-time Developer Notifications (Refund)', + category: 'Refund', + description: + 'Detect Android refunds via voidedPurchaseNotification and SUBSCRIPTION_REVOKED RTDN', + path: '/docs/features/refund#android-rtdn', + }, + { + id: 'refund-android-voided-api', + title: 'Voided Purchases API', + category: 'Refund', + description: 'Poll Google Play Voided Purchases API as a fallback', + path: '/docs/features/refund#android-voided-api', + }, + { + id: 'entitlement-revocation', + title: 'Revoking Entitlements', + category: 'Refund', + description: + 'Revoke user entitlements after a refund is detected — server-side cleanup pattern', + path: '/docs/features/refund#entitlement-revocation', + }, + { + id: 'validation-page', + title: 'Validation', + category: 'Documentation', + description: + 'Server-side purchase validation with verifyPurchase and verifyPurchaseWithProvider (IAPKit)', + path: '/docs/features/validation', + }, + { + id: 'debugging-page', + title: 'Debugging', + category: 'Documentation', + description: + 'Enable OpenIapLog and understand common warnings such as Android basePlanId limitation', + path: '/docs/features/debugging', + }, { id: 'types-page', title: 'Types', @@ -405,18 +604,18 @@ export const apiData: ApiItem[] = [ }, { id: 'subscription-product', - title: 'SubscriptionProduct', + title: 'ProductSubscription', category: 'Types', description: 'Subscription product with pricing phases, intro offers, billing periods', - path: '/docs/types/product#product-subscription', + path: '/docs/types/subscription-product', }, { id: 'storefront', title: 'Storefront', category: 'Types', description: 'Store region info: countryCode returned by getStorefront()', - path: '/docs/types/product#storefront', + path: '/docs/types/storefront', }, { id: 'types-purchase', @@ -439,7 +638,7 @@ export const apiData: ApiItem[] = [ category: 'Types', description: 'Purchase state enum: purchased, pending, failed, restored, deferred', - path: '/docs/types/purchase#purchase-state', + path: '/docs/types/purchase', }, { id: 'active-subscription', @@ -447,7 +646,7 @@ export const apiData: ApiItem[] = [ category: 'Types', description: 'Active subscription: id, productId, isActive from getActiveSubscriptions()', - path: '/docs/types/purchase#active-subscription', + path: '/docs/types/active-subscription', }, { id: 'types-request', @@ -455,7 +654,7 @@ export const apiData: ApiItem[] = [ category: 'Types', description: 'ProductRequest, RequestPurchaseProps, platform-specific request types', - path: '/docs/types/request', + path: '/docs/types/request-purchase-props', }, { id: 'types-verification', @@ -463,7 +662,7 @@ export const apiData: ApiItem[] = [ category: 'Types', description: 'VerifyPurchaseProps, IAPKit integration, purchase verification', - path: '/docs/types/verification', + path: '/docs/types/verify-purchase', }, { id: 'types-ios', @@ -471,14 +670,14 @@ export const apiData: ApiItem[] = [ category: 'Types', description: 'DiscountOffer, SubscriptionStatusIOS, PaymentMode, AppTransaction', - path: '/docs/types/ios', + path: '/docs/types#ios-types', }, { id: 'types-android', title: 'Android Types', category: 'Types', description: 'SubscriptionOffer, PricingPhase, PricingPhasesAndroid', - path: '/docs/types/android', + path: '/docs/types#android-types', }, { id: 'types-alternative', @@ -486,7 +685,7 @@ export const apiData: ApiItem[] = [ category: 'Types', description: 'AlternativeBillingModeAndroid, InitConnectionConfig, External Purchase Link', - path: '/docs/types/alternative', + path: '/docs/types/alternative-billing-types', }, // iOS-Specific Types (from types/ios.tsx) @@ -496,7 +695,7 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS promotional offer for purchase: identifier, keyIdentifier, nonce, signature, timestamp', - path: '/docs/types/ios#discount-offer', + path: '/docs/types/ios/discount-offer-ios', }, { id: 'discount', @@ -511,7 +710,7 @@ export const apiData: ApiItem[] = [ title: 'SubscriptionPeriodIOS', category: 'Types (iOS)', description: 'iOS subscription period units: Day, Week, Month, Year', - path: '/docs/types/ios#subscription-period-ios', + path: '/docs/types/ios/subscription-period-ios', }, { id: 'payment-mode', @@ -519,14 +718,14 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS payment mode for offers: FreeTrial, PayAsYouGo, PayUpFront', - path: '/docs/types/ios#payment-mode', + path: '/docs/types/ios/payment-mode-ios', }, { id: 'subscription-status-ios-type', title: 'SubscriptionStatusIOS', category: 'Types (iOS)', description: 'iOS subscription status from StoreKit 2: state, renewalInfo', - path: '/docs/types/ios#subscription-status-ios', + path: '/docs/types/ios/subscription-status-ios', }, { id: 'app-transaction', @@ -534,7 +733,7 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS app transaction info: bundleId, appVersion, originalAppVersion, environment', - path: '/docs/types/ios#app-transaction', + path: '/docs/types/ios/app-transaction-ios', }, // Android-Specific Types (from types/android.tsx) @@ -544,7 +743,7 @@ export const apiData: ApiItem[] = [ category: 'Types (Android)', description: 'Android subscription offer: sku, offerToken for Play Billing purchases', - path: '/docs/types/android#subscription-offer', + path: '/docs/types/android/subscription-offer-android', }, { id: 'pricing-phase', @@ -552,14 +751,14 @@ export const apiData: ApiItem[] = [ category: 'Types (Android)', description: 'Android pricing phase: billingPeriod, formattedPrice, priceAmountMicros, recurrenceMode', - path: '/docs/types/android#pricing-phase', + path: '/docs/types/android/pricing-phase-android', }, { id: 'pricing-phases-android', title: 'PricingPhasesAndroid', category: 'Types (Android)', description: 'Android pricing phases container: pricingPhaseList array', - path: '/docs/types/android#pricing-phases-android', + path: '/docs/types/android/pricing-phase-android#pricing-phases-android', }, // Alternative Billing Types (from types/alternative.tsx) @@ -568,7 +767,7 @@ export const apiData: ApiItem[] = [ title: 'AlternativeBillingModeAndroid', category: 'Types (Android)', description: 'Android billing mode: NONE, USER_CHOICE, ALTERNATIVE_ONLY', - path: '/docs/types/alternative#alternative-billing-mode-android', + path: '/docs/types/alternative-billing-types', }, { id: 'init-connection-config', @@ -576,7 +775,7 @@ export const apiData: ApiItem[] = [ category: 'Types', description: 'Configuration for initConnection: alternativeBillingModeAndroid', - path: '/docs/types/alternative#init-connection-config', + path: '/docs/types/alternative-billing-types', }, { id: 'external-purchase-link-ios', @@ -584,7 +783,15 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS external purchase APIs: canPresent, presentNoticeSheet, presentLink (iOS 17.4+)', - path: '/docs/types/alternative#external-purchase-link', + path: '/docs/types/external-purchase-link', + }, + { + id: 'billing-programs', + title: 'Billing Programs', + category: 'Types', + description: + 'Android Billing Programs API (Play Billing 8.2.0+): BillingProgramAndroid, ExternalLink launch modes, Developer Provided Billing parameters', + path: '/docs/types/billing-programs', }, // Platform-Specific Request Types @@ -594,7 +801,7 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS purchase request parameters: sku, appAccountToken, quantity, withOffer', - path: '/docs/types/request#request-purchase-ios-props', + path: '/docs/types/request-purchase-props', }, { id: 'request-purchase-android-props', @@ -602,7 +809,7 @@ export const apiData: ApiItem[] = [ category: 'Types (Android)', description: 'Android purchase request parameters: skus, obfuscatedAccountId, isOfferPersonalized', - path: '/docs/types/request#request-purchase-android-props', + path: '/docs/types/request-purchase-props', }, { id: 'request-subscription-ios-props', @@ -610,7 +817,7 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS subscription request parameters (same as RequestPurchaseIosProps)', - path: '/docs/types/request#request-subscription-ios-props', + path: '/docs/types/request-purchase-props', }, { id: 'request-subscription-android-props', @@ -618,7 +825,7 @@ export const apiData: ApiItem[] = [ category: 'Types (Android)', description: 'Android subscription request: purchaseToken, replacementMode, subscriptionOffers', - path: '/docs/types/request#request-subscription-android-props', + path: '/docs/types/request-purchase-props', }, // Platform-Specific Product Types @@ -640,18 +847,18 @@ export const apiData: ApiItem[] = [ }, { id: 'subscription-product-ios', - title: 'SubscriptionProductIOS', + title: 'ProductSubscriptionIOS', category: 'Types (iOS)', description: 'iOS subscription fields: subscriptionOffers, introductoryPriceIOS, subscriptionPeriodUnitIOS', - path: '/docs/types/product#subscription-product-ios', + path: '/docs/types/subscription-product#subscription-product-ios', }, { id: 'subscription-product-android', - title: 'SubscriptionProductAndroid', + title: 'ProductSubscriptionAndroid', category: 'Types (Android)', description: 'Android subscription fields: subscriptionOffers', - path: '/docs/types/product#subscription-product-android', + path: '/docs/types/subscription-product#subscription-product-android', }, // Platform-Specific Purchase Types @@ -677,7 +884,7 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS subscription renewal info: willAutoRenew, expirationReason, gracePeriodExpirationDate', - path: '/docs/types/purchase#renewal-info-ios', + path: '/docs/types/ios/renewal-info-ios', }, { id: 'active-subscription-ios', @@ -685,7 +892,7 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS active subscription: expirationDateIOS, environmentIOS, daysUntilExpirationIOS', - path: '/docs/types/purchase#active-subscription-ios', + path: '/docs/types/active-subscription#active-subscription-ios', }, { id: 'active-subscription-android', @@ -693,7 +900,7 @@ export const apiData: ApiItem[] = [ category: 'Types (Android)', description: 'Android active subscription: autoRenewingAndroid, basePlanIdAndroid, purchaseTokenAndroid', - path: '/docs/types/purchase#active-subscription-android', + path: '/docs/types/active-subscription#active-subscription-android', }, // Platform-Specific Verification Types @@ -703,7 +910,7 @@ export const apiData: ApiItem[] = [ category: 'Types (iOS)', description: 'iOS verification result: isValid, receiptData, jwsRepresentation, latestTransaction', - path: '/docs/types/verification#verify-purchase-result-ios', + path: '/docs/types/verify-purchase#verify-purchase-result-ios', }, { id: 'verify-purchase-result-android', @@ -711,14 +918,14 @@ export const apiData: ApiItem[] = [ category: 'Types (Android)', description: 'Android verification result: autoRenewing, cancelDate, renewalDate, transactionId', - path: '/docs/types/verification#verify-purchase-result-android', + path: '/docs/types/verify-purchase#verify-purchase-result-android', }, { id: 'verify-purchase-result-horizon', title: 'VerifyPurchaseResultHorizon', category: 'Types (Horizon)', description: 'Meta Quest verification result: success, grantTime', - path: '/docs/types/verification#verify-purchase-result-horizon', + path: '/docs/types/verify-purchase#verify-purchase-result-horizon', }, { diff --git a/packages/docs/src/pages/docs/apis/android.tsx b/packages/docs/src/pages/docs/apis/android.tsx deleted file mode 100644 index 56295dfb9..000000000 --- a/packages/docs/src/pages/docs/apis/android.tsx +++ /dev/null @@ -1,630 +0,0 @@ -import { Link } from 'react-router-dom'; -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function AndroidAPIs() { - useScrollToHash(); - - return ( -
    - -

    Android Specific

    -

    - Android-specific APIs using Google Play Billing Library. These APIs are - only available on Android and end with the Android suffix. -

    - - - - - -
    - - Purchase Completion - - - - acknowledgePurchaseAndroid - -

    - Acknowledge a non-consumable purchase or subscription. Required within - 3 days or the purchase will be refunded. -

    - {`suspend fun acknowledgePurchase(purchaseToken: String): Boolean`} -

    - Note: This is called automatically by{' '} - - finishTransaction() - {' '} - when isConsumable is false. -

    - - - consumePurchaseAndroid - -

    - Consume a consumable purchase, allowing repurchase. Automatically - acknowledges the purchase. -

    - {`suspend fun consumePurchase(purchaseToken: String): Boolean`} -

    - Note: This is called automatically by{' '} - - finishTransaction() - {' '} - when isConsumable is true. -

    -
    - -
    - - Alternative Billing APIs - -

    - Three-step flow for implementing alternative billing on Android. These - APIs work with Google Play Billing Library 6.2+. -

    - -
    -

    - Configuration: Enable alternative billing mode - during{' '} - - initConnection() - {' '} - with alternativeBillingModeAndroid. -

    -
    - - - checkAlternativeBillingAvailabilityAndroid - -

    - Step 1: Check if alternative billing is available for - this user/device. -

    - {`// Returns true if available, false otherwise -// Throws OpenIapError.NotPrepared if billing client not ready -suspend fun checkAlternativeBillingAvailability(): Boolean`} - - - showAlternativeBillingDialogAndroid - -

    - Step 2: Show alternative billing information dialog - to the user. Must be called before processing payment - in your payment system. -

    - {`// Returns true if user accepted, false if user canceled -// Throws OpenIapError.NotPrepared if billing client not ready -suspend fun showAlternativeBillingDialog(): Boolean`} - - - createAlternativeBillingTokenAndroid - -

    - Step 3: Create external transaction token for Google - Play reporting. Must be called after successful - payment in your payment system. -

    - {`// Token must be reported to Google Play backend within 24 hours -// Returns token string, or null if creation failed -suspend fun createAlternativeBillingToken(): String?`} -
    - -
    - - Alternative Billing Flow Example - - - {{ - typescript: ( - {`import { - checkAlternativeBillingAvailabilityAndroid, - showAlternativeBillingDialogAndroid, - createAlternativeBillingTokenAndroid, -} from 'expo-iap'; - -async function handleAlternativePurchase() { - // Step 1: Check availability - const isAvailable = await checkAlternativeBillingAvailabilityAndroid(); - if (!isAvailable) { - // Fall back to standard billing - return; - } - - // Step 2: Show dialog to user - const userAccepted = await showAlternativeBillingDialogAndroid(); - if (!userAccepted) { - // User canceled - return; - } - - // Process payment in your payment system - const paymentSuccess = await processPaymentInYourSystem(); - - if (paymentSuccess) { - // Step 3: Create token and report to Google Play - const token = await createAlternativeBillingTokenAndroid(); - - if (token) { - // Report token to Google Play backend within 24 hours - await reportTokenToGooglePlay(token); - } - } -}`} - ), - kotlin: ( - {`// Step 1: Check availability -val isAvailable = openIapStore.checkAlternativeBillingAvailability() -if (!isAvailable) { - // Fall back to standard billing - return -} - -// Step 2: Show dialog to user -val userAccepted = openIapStore.showAlternativeBillingDialog() -if (!userAccepted) { - // User canceled - return -} - -// Process payment in your payment system -val paymentSuccess = processPaymentInYourSystem() - -if (paymentSuccess) { - // Step 3: Create token and report to Google Play - val token = openIapStore.createAlternativeBillingToken() - - token?.let { - // Report token to Google Play backend within 24 hours - reportTokenToGooglePlay(it) - } -}`} - ), - dart: ( - {`// Step 1: Check availability -final isAvailable = await FlutterInappPurchase.instance - .checkAlternativeBillingAvailabilityAndroid(); -if (!isAvailable) { - return; // Fall back to standard billing -} - -// Step 2: Show dialog to user -final userAccepted = await FlutterInappPurchase.instance - .showAlternativeBillingDialogAndroid(); -if (!userAccepted) { - return; // User canceled -} - -// Process payment in your payment system -final paymentSuccess = await processPaymentInYourSystem(); - -if (paymentSuccess) { - // Step 3: Create token and report to Google Play - final token = await FlutterInappPurchase.instance - .createAlternativeBillingTokenAndroid(); - - if (token != null) { - await reportTokenToGooglePlay(token); - } -}`} - ), - gdscript: ( - {`func handle_alternative_purchase(): - # Step 1: Check availability - var is_available = await iap.check_alternative_billing_availability_android() - if not is_available: - # Fall back to standard billing - return - - # Step 2: Show dialog to user - var user_accepted = await iap.show_alternative_billing_dialog_android() - if not user_accepted: - # User canceled - return - - # Process payment in your payment system - var payment_success = await process_payment_in_your_system() - - if payment_success: - # Step 3: Create token and report to Google Play - var token = await iap.create_alternative_billing_token_android() - - if token: - # Report token to Google Play backend within 24 hours - await report_token_to_google_play(token)`} - ), - }} - - -
    -

    - Important: The token from Step 3 must be reported - to Google Play backend within 24 hours. See{' '} - - Google's Alternative Billing documentation - {' '} - for backend integration details. -

    -
    - -
    -

    - Deprecated: The above APIs are deprecated in Google - Play Billing Library 8.2.0+. For new implementations, use the{' '} - Billing Programs API below. -

    -
    -
    - -
    - - Billing Programs API (8.2.0+) - -

    - Google Play Billing Library 8.2.0 introduces the new Billing Programs - API which replaces the legacy alternative billing APIs. This provides - better support for External Content Links and External Offers. -

    - -
    -

    - Recommended: Use Billing Library 8.2.1+ as version - 8.2.0 had bugs in isBillingProgramAvailableAsync and{' '} - createBillingProgramReportingDetailsAsync. -

    -
    - - - enableBillingProgramAndroid - -

    - Step 0: Enable a billing program before calling{' '} - initConnection(). Must be called during BillingClient - setup. -

    - {`// Call BEFORE initConnection() -// program: BillingProgramAndroid.ExternalOffer or BillingProgramAndroid.ExternalContentLink -fun enableBillingProgram(program: BillingProgramAndroid)`} - - - isBillingProgramAvailableAndroid - -

    - Step 1: Check if a billing program is available for - the current user. -

    - {`// Returns BillingProgramAvailabilityResultAndroid with isAvailable flag -// Throws OpenIapError.NotPrepared if billing client not ready -suspend fun isBillingProgramAvailable( - program: BillingProgramAndroid -): BillingProgramAvailabilityResultAndroid`} - - - launchExternalLinkAndroid - -

    - Step 2: Launch external link flow. Shows Play Store - dialog and optionally launches external URL. -

    - {`// Returns true if launched successfully -// Throws OpenIapError.NotPrepared if billing client not ready -suspend fun launchExternalLink( - activity: Activity, - params: LaunchExternalLinkParamsAndroid -): Boolean - -// LaunchExternalLinkParamsAndroid: -// - billingProgram: BillingProgramAndroid (ExternalOffer or ExternalContentLink) -// - launchMode: ExternalLinkLaunchModeAndroid -// - linkType: ExternalLinkTypeAndroid -// - linkUri: String (your external URL)`} - - - createBillingProgramReportingDetailsAndroid - -

    - Step 3: Create reporting details after successful - payment. Returns external transaction token for reporting. -

    -
    -

    - Note: This API uses{' '} - BillingProgramReportingDetailsParams internally, which - requires Billing Library 8.3.0+. OpenIAP handles this automatically. -

    -
    - {`// Returns BillingProgramReportingDetailsAndroid with externalTransactionToken -// Token must be reported to Google Play backend within 24 hours -// Throws OpenIapError.NotPrepared if billing client not ready -suspend fun createBillingProgramReportingDetails( - program: BillingProgramAndroid -): BillingProgramReportingDetailsAndroid`} -
    - -
    - - Billing Programs Flow Example - - - {{ - typescript: ( - {`import { - enableBillingProgramAndroid, - isBillingProgramAvailableAndroid, - launchExternalLinkAndroid, - createBillingProgramReportingDetailsAndroid, - initConnection, -} from 'expo-iap'; - -// Step 0: Enable billing program BEFORE initConnection -enableBillingProgramAndroid('EXTERNAL_OFFER'); - -await initConnection(); - -async function handleExternalPurchase() { - // Step 1: Check availability - const result = await isBillingProgramAvailableAndroid('EXTERNAL_OFFER'); - if (!result.isAvailable) { - return; // Not available for this user - } - - // Step 2: Launch external link - const launched = await launchExternalLinkAndroid({ - billingProgram: 'EXTERNAL_OFFER', - launchMode: 'LAUNCH_IN_EXTERNAL_BROWSER_OR_APP', - linkType: 'LINK_TO_DIGITAL_CONTENT_OFFER', - linkUri: 'https://your-payment-site.com/checkout', - }); - - if (!launched) { - return; // Failed to launch - } - - // Process payment in your payment system - const paymentSuccess = await processPaymentInYourSystem(); - - if (paymentSuccess) { - // Step 3: Create reporting details - const details = await createBillingProgramReportingDetailsAndroid('EXTERNAL_OFFER'); - - // Report token to Google Play backend within 24 hours - await reportTokenToGooglePlay(details.externalTransactionToken); - } -}`} - ), - kotlin: ( - {`// Step 0: Enable billing program BEFORE initConnection -openIapStore.enableBillingProgram(BillingProgramAndroid.ExternalOffer) - -openIapStore.initConnection(null) - -suspend fun handleExternalPurchase() { - // Step 1: Check availability - val result = openIapStore.isBillingProgramAvailable( - BillingProgramAndroid.ExternalOffer - ) - if (!result.isAvailable) { - return // Not available for this user - } - - // Step 2: Launch external link - val launched = openIapStore.launchExternalLink( - activity, - LaunchExternalLinkParamsAndroid( - billingProgram = BillingProgramAndroid.ExternalOffer, - launchMode = ExternalLinkLaunchModeAndroid.LaunchInExternalBrowserOrApp, - linkType = ExternalLinkTypeAndroid.LinkToDigitalContentOffer, - linkUri = "https://your-payment-site.com/checkout" - ) - ) - - if (!launched) { - return // Failed to launch - } - - // Process payment in your payment system - val paymentSuccess = processPaymentInYourSystem() - - if (paymentSuccess) { - // Step 3: Create reporting details - val details = openIapStore.createBillingProgramReportingDetails( - BillingProgramAndroid.ExternalOffer - ) - - // Report token to Google Play backend within 24 hours - reportTokenToGooglePlay(details.externalTransactionToken) - } -}`} - ), - dart: ( - {`// Step 0: Enable billing program BEFORE initConnection -FlutterInappPurchase.instance.enableBillingProgramAndroid( - BillingProgramAndroid.externalOffer, -); - -await FlutterInappPurchase.instance.initConnection(); - -Future handleExternalPurchase() async { - // Step 1: Check availability - final result = await FlutterInappPurchase.instance - .isBillingProgramAvailableAndroid(BillingProgramAndroid.externalOffer); - if (!result.isAvailable) { - return; // Not available for this user - } - - // Step 2: Launch external link - final launched = await FlutterInappPurchase.instance.launchExternalLinkAndroid( - LaunchExternalLinkParamsAndroid( - billingProgram: BillingProgramAndroid.externalOffer, - launchMode: ExternalLinkLaunchModeAndroid.launchInExternalBrowserOrApp, - linkType: ExternalLinkTypeAndroid.linkToDigitalContentOffer, - linkUri: 'https://your-payment-site.com/checkout', - ), - ); - - if (!launched) { - return; // Failed to launch - } - - // Process payment in your payment system - final paymentSuccess = await processPaymentInYourSystem(); - - if (paymentSuccess) { - // Step 3: Create reporting details - final details = await FlutterInappPurchase.instance - .createBillingProgramReportingDetailsAndroid( - BillingProgramAndroid.externalOffer, - ); - - // Report token to Google Play backend within 24 hours - await reportTokenToGooglePlay(details.externalTransactionToken); - } -}`} - ), - gdscript: ( - {`# Step 0: Enable billing program BEFORE initConnection -func _ready() -> void: - iap.enable_billing_program_android(BillingProgramAndroid.EXTERNAL_OFFER) - await iap.init_connection() - -func handle_external_purchase(): - # Step 1: Check availability - var result = await iap.is_billing_program_available_android( - BillingProgramAndroid.EXTERNAL_OFFER - ) - if not result.is_available: - return # Not available for this user - - # Step 2: Launch external link - var params = LaunchExternalLinkParamsAndroid.new() - params.billing_program = BillingProgramAndroid.EXTERNAL_OFFER - params.launch_mode = ExternalLinkLaunchModeAndroid.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP - params.link_type = ExternalLinkTypeAndroid.LINK_TO_DIGITAL_CONTENT_OFFER - params.link_uri = "https://your-payment-site.com/checkout" - - var launched = await iap.launch_external_link_android(params) - - if not launched: - return # Failed to launch - - # Process payment in your payment system - var payment_success = await process_payment_in_your_system() - - if payment_success: - # Step 3: Create reporting details - var details = await iap.create_billing_program_reporting_details_android( - BillingProgramAndroid.EXTERNAL_OFFER - ) - - # Report token to Google Play backend within 24 hours - await report_token_to_google_play(details.external_transaction_token)`} - ), - }} - -
    - -
    - - API Migration Guide - -

    - Migrate from legacy Alternative Billing APIs to Billing Programs API: -

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    Legacy API (6.2+)New API (8.2.0+)
    - checkAlternativeBillingAvailability() - - isBillingProgramAvailable(program) -
    - showAlternativeBillingInformationDialog() - - launchExternalLink(activity, params) -
    - createAlternativeBillingReportingToken() - - createBillingProgramReportingDetails(program) -
    - enableAlternativeBillingOnly() - - enableBillingProgram(program) -
    - -
    -

    - See Also:{' '} - - External Purchase Guide - {' '} - for complete implementation details and examples. -

    -
    -
    -
    - ); -} - -export default AndroidAPIs; diff --git a/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx new file mode 100644 index 000000000..9c047fcd3 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/acknowledge-purchase-android.tsx @@ -0,0 +1,74 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function AcknowledgePurchaseAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + acknowledgePurchaseAndroid +

    +

    + Acknowledge a non-consumable purchase or subscription. Required within 3 + days or the purchase will be refunded. +

    + +
    +

    + ⚠️ Deprecated in Google Play Billing Library 8.2.0+.{' '} + Direct acknowledge / consume calls are being phased out — use the + cross-platform{' '} + + finishTransaction + {' '} + API instead, which handles acknowledgment automatically and is the + recommended path for new code. +

    +
    + +

    Signature

    + + {{ + typescript: ( + {`acknowledgePurchaseAndroid(purchaseToken: string): Promise`} + ), + kotlin: ( + {`suspend fun acknowledgePurchase(purchaseToken: String): Boolean`} + ), + kmp: ( + {`suspend fun acknowledgePurchaseAndroid(purchaseToken: String): Boolean`} + ), + dart: ( + {`Future acknowledgePurchaseAndroid(String purchaseToken);`} + ), + gdscript: ( + {`func acknowledge_purchase_android(purchase_token: String) -> bool`} + ), + }} + + +

    + Note: Called automatically by{' '} + finishTransaction() when{' '} + isConsumable is false. +

    + +

    + See: finishTransaction +

    +
    + ); +} + +export default AcknowledgePurchaseAndroid; diff --git a/packages/docs/src/pages/docs/apis/android/check-alternative-billing-availability-android.tsx b/packages/docs/src/pages/docs/apis/android/check-alternative-billing-availability-android.tsx new file mode 100644 index 000000000..96138d87c --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/check-alternative-billing-availability-android.tsx @@ -0,0 +1,40 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function CheckAlternativeBillingAvailabilityAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + checkAlternativeBillingAvailabilityAndroid +

    +

    + Step 1 of alternative billing flow. Check if alternative billing is + available for this user/device. +

    + +

    Signature

    + + {{ + kotlin: ( + {`// Returns true if available, false otherwise +// Throws OpenIapError.NotPrepared if billing client not ready +suspend fun checkAlternativeBillingAvailability(): Boolean`} + ), + }} + +
    + ); +} + +export default CheckAlternativeBillingAvailabilityAndroid; diff --git a/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx new file mode 100644 index 000000000..4f3fa0a25 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/consume-purchase-android.tsx @@ -0,0 +1,62 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function ConsumePurchaseAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + consumePurchaseAndroid +

    +

    + Consume a consumable purchase, allowing repurchase. Automatically + acknowledges the purchase. +

    + +
    +

    + ⚠️ Deprecated in Google Play Billing Library 8.2.0+.{' '} + Use{' '} + + finishTransaction + {' '} + with isConsumable: true instead — the unified path + consumes (or acknowledges) the purchase automatically and stays + forward-compatible with the new Billing Programs API. +

    +
    + +

    Signature

    + + {{ + kotlin: ( + {`suspend fun consumePurchase(purchaseToken: String): Boolean`} + ), + }} + + +

    + Note: Called automatically by{' '} + finishTransaction() when{' '} + isConsumable is true. +

    + +

    + See: finishTransaction +

    +
    + ); +} + +export default ConsumePurchaseAndroid; diff --git a/packages/docs/src/pages/docs/apis/android/create-alternative-billing-token-android.tsx b/packages/docs/src/pages/docs/apis/android/create-alternative-billing-token-android.tsx new file mode 100644 index 000000000..1160020a9 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/create-alternative-billing-token-android.tsx @@ -0,0 +1,40 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function CreateAlternativeBillingTokenAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + createAlternativeBillingTokenAndroid +

    +

    + Step 3 of alternative billing flow. Create external transaction token + for Google Play reporting. +

    + +

    Signature

    + + {{ + kotlin: ( + {`// Token must be reported to Google Play backend within 24 hours +// Returns token string, or null if creation failed +suspend fun createAlternativeBillingToken(): String?`} + ), + }} + +
    + ); +} + +export default CreateAlternativeBillingTokenAndroid; diff --git a/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx b/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx new file mode 100644 index 000000000..7cf943d81 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/create-billing-program-reporting-details-android.tsx @@ -0,0 +1,43 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function CreateBillingProgramReportingDetailsAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + createBillingProgramReportingDetailsAndroid +

    +

    + Step 3 of Billing Programs API. Create reporting details with external + transaction token after successful payment. +

    + +

    Signature

    + + {{ + kotlin: ( + {`// Returns BillingProgramReportingDetailsAndroid with externalTransactionToken +// Token must be reported to Google Play backend within 24 hours +// Throws OpenIapError.NotPrepared if billing client not ready +suspend fun createBillingProgramReportingDetails( + program: BillingProgramAndroid +): BillingProgramReportingDetailsAndroid`} + ), + }} + +
    + ); +} + +export default CreateBillingProgramReportingDetailsAndroid; diff --git a/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx b/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx new file mode 100644 index 000000000..39452d6c3 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/enable-billing-program-android.tsx @@ -0,0 +1,87 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function EnableBillingProgramAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + enableBillingProgramAndroid +

    +

    + Enables a billing program for Android (Billing Library 8.2.0+). Pass it + as the{' '} + + enableBillingProgramAndroid + {' '} + field of{' '} + + InitConnectionConfig + {' '} + when calling initConnection() — there is no separate + top-level call. +

    + +

    Signature

    + + {{ + typescript: ( + {`// expo-iap +import { initConnection } from 'expo-iap'; +// Same API in react-native-iap: +// import { initConnection } from 'react-native-iap'; + +await initConnection({ + enableBillingProgramAndroid: 'external-offer', + // 'user-choice-billing' | 'external-content-link' | 'external-offer' | 'external-payments' +}); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP auto-connects on mount and accepts the same enableBillingProgramAndroid +// option directly, so the billing program is wired without an explicit +// initConnection() call. +import { useIAP } from 'expo-iap'; + +function App() { + useIAP({ enableBillingProgramAndroid: 'external-offer' }); + + return ; +}`} + ), + kotlin: ( + {`openIapStore.initConnection( + InitConnectionConfig( + enableBillingProgramAndroid = BillingProgramAndroid.ExternalOffer + ) +)`} + ), + dart: ( + {`await FlutterInappPurchase.instance.initConnection( + config: InitConnectionConfig( + enableBillingProgramAndroid: BillingProgramAndroid.externalOffer, + ), +);`} + ), + gdscript: ( + {`var config = InitConnectionConfig.new() +config.enable_billing_program_android = BillingProgramAndroid.EXTERNAL_OFFER +await iap.init_connection(config)`} + ), + }} + +
    + ); +} + +export default EnableBillingProgramAndroid; diff --git a/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx b/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx new file mode 100644 index 000000000..caee40b37 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/is-billing-program-available-android.tsx @@ -0,0 +1,42 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function IsBillingProgramAvailableAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + isBillingProgramAvailableAndroid +

    +

    + Step 1 of Billing Programs API. Check if a billing program is available + for the current user. +

    + +

    Signature

    + + {{ + kotlin: ( + {`// Returns BillingProgramAvailabilityResultAndroid with isAvailable flag +// Throws OpenIapError.NotPrepared if billing client not ready +suspend fun isBillingProgramAvailable( + program: BillingProgramAndroid +): BillingProgramAvailabilityResultAndroid`} + ), + }} + +
    + ); +} + +export default IsBillingProgramAvailableAndroid; diff --git a/packages/docs/src/pages/docs/apis/android/launch-external-link-android.tsx b/packages/docs/src/pages/docs/apis/android/launch-external-link-android.tsx new file mode 100644 index 000000000..135c448bb --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/launch-external-link-android.tsx @@ -0,0 +1,49 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function LaunchExternalLinkAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + launchExternalLinkAndroid +

    +

    + Step 2 of Billing Programs API. Launch external link flow — shows Play + Store dialog and optionally launches external URL. +

    + +

    Signature

    + + {{ + kotlin: ( + {`// Returns true if launched successfully +// Throws OpenIapError.NotPrepared if billing client not ready +suspend fun launchExternalLink( + activity: Activity, + params: LaunchExternalLinkParamsAndroid +): Boolean + +// LaunchExternalLinkParamsAndroid: +// - billingProgram: BillingProgramAndroid +// - launchMode: ExternalLinkLaunchModeAndroid +// - linkType: ExternalLinkTypeAndroid +// - linkUri: String`} + ), + }} + +
    + ); +} + +export default LaunchExternalLinkAndroid; diff --git a/packages/docs/src/pages/docs/apis/android/show-alternative-billing-dialog-android.tsx b/packages/docs/src/pages/docs/apis/android/show-alternative-billing-dialog-android.tsx new file mode 100644 index 000000000..143c5ba8f --- /dev/null +++ b/packages/docs/src/pages/docs/apis/android/show-alternative-billing-dialog-android.tsx @@ -0,0 +1,40 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function ShowAlternativeBillingDialogAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + showAlternativeBillingDialogAndroid +

    +

    + Step 2 of alternative billing flow. Show alternative billing information + dialog before processing payment. +

    + +

    Signature

    + + {{ + kotlin: ( + {`// Returns true if user accepted, false if user canceled +// Throws OpenIapError.NotPrepared if billing client not ready +suspend fun showAlternativeBillingDialog(): Boolean`} + ), + }} + +
    + ); +} + +export default ShowAlternativeBillingDialogAndroid; diff --git a/packages/docs/src/pages/docs/apis/connection.tsx b/packages/docs/src/pages/docs/apis/connection.tsx deleted file mode 100644 index 17cc5e200..000000000 --- a/packages/docs/src/pages/docs/apis/connection.tsx +++ /dev/null @@ -1,223 +0,0 @@ -import { Link } from 'react-router-dom'; -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function ConnectionAPIs() { - useScrollToHash(); - - return ( -
    - -

    Connection APIs

    -

    - Manage the connection to the platform's billing service. These APIs must - be called before any other IAP operations. -

    - - - - - -
    - - initConnection - -

    - Initialize connection to the store service. Must be called before any - other IAP operations. -

    - -

    Signature

    - - {{ - typescript: ( - {`initConnection(config?: InitConnectionConfig): Promise - -interface InitConnectionConfig { - alternativeBillingModeAndroid?: 'user-choice' | 'alternative-only'; -}`} - ), - swift: ( - {`func initConnection() async throws -> Bool`} - ), - kotlin: ( - {`suspend fun initConnection(config: InitConnectionConfig? = null): Boolean`} - ), - kmp: ( - {`suspend fun initConnection(config: InitConnectionConfig? = null): Boolean`} - ), - dart: ( - {`Future initConnection({InitConnectionConfig? config});`} - ), - gdscript: ( - {`func init_connection(config: InitConnectionConfig = null) -> bool`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { initConnection } from 'expo-iap'; - -// Standard connection -await initConnection(); - -// Android with user choice billing -await initConnection({ - alternativeBillingModeAndroid: 'user-choice' -});`} - ), - swift: ( - {`import OpenIap - -try await OpenIapModule.shared.initConnection()`} - ), - kotlin: ( - {`// Standard connection -openIapStore.initConnection() - -// With alternative billing -openIapStore.initConnection( - InitConnectionConfig( - alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice - ) -)`} - ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -// Standard connection -kmpIAP.initConnection() - -// With alternative billing -kmpIAP.initConnection( - InitConnectionConfig( - alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice - ) -)`} - ), - dart: ( - {`await FlutterInappPurchase.instance.initConnection();`} - ), - gdscript: ( - {`# Standard connection -var success = await iap.init_connection() - -# With alternative billing (Android) -var config = InitConnectionConfig.new() -config.alternative_billing_mode_android = AlternativeBillingModeAndroid.USER_CHOICE -var success = await iap.init_connection(config)`} - ), - }} - - -

    - See:{' '} - - InitConnectionConfig - -

    -
    - -
    - - endConnection - -

    - End connection to the store service. Call this when your app closes or - the IAP component unmounts to clean up resources. -

    - -

    Signature

    - - {{ - typescript: ( - {`endConnection(): Promise`} - ), - swift: ( - {`func endConnection() async throws -> Bool`} - ), - kotlin: ( - {`suspend fun endConnection(): Boolean`} - ), - kmp: ( - {`suspend fun endConnection(): Boolean`} - ), - dart: ( - {`Future endConnection();`} - ), - gdscript: ( - {`func end_connection() -> bool`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { endConnection } from 'expo-iap'; - -// In React useEffect cleanup -useEffect(() => { - void initConnection(); - - return () => { - void endConnection(); - }; -}, []);`} - ), - swift: ( - {`try await OpenIapModule.shared.endConnection()`} - ), - kotlin: ( - {`openIapStore.endConnection()`} - ), - kmp: ( - {`kmpIAP.endConnection()`} - ), - dart: ( - {`await FlutterInappPurchase.instance.endConnection();`} - ), - gdscript: ( - {`# In _exit_tree or cleanup -func _exit_tree(): - await iap.end_connection()`} - ), - }} - -
    -
    - ); -} - -export default ConnectionAPIs; diff --git a/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx b/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx new file mode 100644 index 000000000..91666e9da --- /dev/null +++ b/packages/docs/src/pages/docs/apis/deep-link-to-subscriptions.tsx @@ -0,0 +1,133 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function DeepLinkToSubscriptions() { + useScrollToHash(); + + return ( +
    + +

    deepLinkToSubscriptions

    +

    + Open the native subscription management interface where users can view + and manage their subscriptions. +

    + +

    Signature

    + + {{ + typescript: ( + {`deepLinkToSubscriptions(options?: DeepLinkOptions): Promise + +interface DeepLinkOptions { + skuAndroid?: string; + packageNameAndroid?: string; +}`} + ), + swift: ( + {`func deepLinkToSubscriptions() async throws`} + ), + kotlin: ( + {`suspend fun deepLinkToSubscriptions(options: DeepLinkOptions? = null)`} + ), + kmp: ( + {`suspend fun deepLinkToSubscriptions(options: DeepLinkOptions? = null)`} + ), + dart: ( + {`Future deepLinkToSubscriptions({String? skuAndroid, String? packageNameAndroid});`} + ), + gdscript: ( + {`func deep_link_to_subscriptions(options: DeepLinkOptions) -> void`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { deepLinkToSubscriptions } from 'expo-iap'; +// Same API in react-native-iap: +// import { deepLinkToSubscriptions } from 'react-native-iap'; + +await deepLinkToSubscriptions({ + skuAndroid: 'com.app.premium', + packageNameAndroid: 'com.yourcompany.app', +}); + +// --- Or alongside the useIAP() hook (also exported from react-native-iap) --- +// deepLinkToSubscriptions is a module-level helper; useIAP doesn't expose it +// on the hook return, so call the module function from inside your +// component (the hook still owns the connection lifecycle). +import { useIAP } from 'expo-iap'; + +function ManageSubscriptionsButton() { + useIAP(); + + return ( +
    + ); +} + +export default DeepLinkToSubscriptions; diff --git a/packages/docs/src/pages/docs/apis/end-connection.tsx b/packages/docs/src/pages/docs/apis/end-connection.tsx new file mode 100644 index 000000000..5e38b300a --- /dev/null +++ b/packages/docs/src/pages/docs/apis/end-connection.tsx @@ -0,0 +1,101 @@ +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function EndConnection() { + useScrollToHash(); + + return ( +
    + +

    endConnection

    +

    + End connection to the store service. Call this when your app closes or + the IAP component unmounts to clean up resources. +

    + +

    Signature

    + + {{ + typescript: ( + {`endConnection(): Promise`} + ), + swift: ( + {`func endConnection() async throws -> Bool`} + ), + kotlin: ( + {`suspend fun endConnection(): Boolean`} + ), + kmp: ( + {`suspend fun endConnection(): Boolean`} + ), + dart: ( + {`Future endConnection();`} + ), + gdscript: ( + {`func end_connection() -> bool`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { endConnection } from 'expo-iap'; +// Same API in react-native-iap: +// import { endConnection } from 'react-native-iap'; + +// In React useEffect cleanup +useEffect(() => { + void initConnection(); + + return () => { + void endConnection(); + }; +}, []); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP automatically calls endConnection() when the component unmounts, +// so you only need the module-level call when you want to tear the +// connection down outside of the hook's lifecycle (e.g. on sign-out). +import { useIAP } from 'expo-iap'; + +function PurchaseScreen() { + const { connected } = useIAP(); + + // No explicit endConnection() call needed — the hook handles cleanup. + return Store ready: {String(connected)}; +}`} + ), + swift: ( + {`try await OpenIapModule.shared.endConnection()`} + ), + kotlin: ( + {`openIapStore.endConnection()`} + ), + kmp: ( + {`kmpIAP.endConnection()`} + ), + dart: ( + {`await FlutterInappPurchase.instance.endConnection();`} + ), + gdscript: ( + {`# In _exit_tree or cleanup +func _exit_tree(): + await iap.end_connection()`} + ), + }} + +
    + ); +} + +export default EndConnection; diff --git a/packages/docs/src/pages/docs/apis/fetch-products.tsx b/packages/docs/src/pages/docs/apis/fetch-products.tsx new file mode 100644 index 000000000..4245acaf6 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/fetch-products.tsx @@ -0,0 +1,198 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function FetchProducts() { + useScrollToHash(); + + return ( +
    + +

    fetchProducts

    +

    Retrieve products or subscriptions from the store by SKU.

    + + + Note about request* APIs + +
    +

    + ℹ️{' '} + This note is about sibling APIs, not fetchProducts.{' '} + fetchProducts itself is a regular promise-based call — + its Promise<FetchProductsResult> return value{' '} + is the canonical way to read the products you queried. +

    +

    + Reader pitfall to be aware of: APIs in this library that do{' '} + start with request ( + + requestPurchase + + , requestPurchaseOnPromotedProductIOS) are{' '} + event-based. Their return values are not the purchase + result — listen via{' '} + + purchaseUpdatedListener + {' '} + /{' '} + + purchaseErrorListener + {' '} + instead. This is because Apple's purchase system is fundamentally + event-based; see{' '} + + this issue comment + + . +

    +
    + +

    Signature

    + + {{ + typescript: ( + {`fetchProducts(params: ProductRequest): Promise + +interface ProductRequest { + skus: string[]; + type?: 'in-app' | 'subs' | 'all'; // Defaults to 'in-app' +} + +// FetchProductsResult is the union returned by the canonical schema — +// the variant depends on the request \`type\`. +type FetchProductsResult = + | Product[] + | ProductSubscription[] + | ProductOrSubscription[] + | null;`} + ), + swift: ( + {`func fetchProducts(_ params: ProductRequest) async throws -> FetchProductsResult`} + ), + kotlin: ( + {`suspend fun fetchProducts(request: ProductRequest): FetchProductsResult`} + ), + kmp: ( + {`suspend fun fetchProducts(request: ProductRequest): FetchProductsResult`} + ), + dart: ( + {`Future fetchProducts({ + required List skus, + ProductQueryType? type, +});`} + ), + gdscript: ( + {`# Returns Array[Product] for IN_APP, Array[ProductSubscription] for SUBS, +# or a mixed Array for ALL — typed as Array because GDScript can't express +# heterogeneous element types. +func fetch_products(request: ProductRequest) -> Array`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { fetchProducts } from 'expo-iap'; +// Same API in react-native-iap: +// import { fetchProducts } from 'react-native-iap'; + +// Fetch one-time products +const products = await fetchProducts({ + skus: ['com.app.coins_100', 'com.app.premium'], + type: 'in-app', +}); + +// Fetch subscriptions +const subscriptions = await fetchProducts({ + skus: ['com.app.monthly', 'com.app.yearly'], + type: 'subs', +}); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// The hook exposes fetchProducts plus a reactive products array that is +// populated whenever fetchProducts resolves. +import { useIAP } from 'expo-iap'; + +function ProductList() { + const { products, fetchProducts } = useIAP(); + + useEffect(() => { + void fetchProducts({ + skus: ['com.app.coins_100', 'com.app.premium'], + type: 'in-app', + }); + }, [fetchProducts]); + + return ( + + {products.map((p) => ( + {p.title} — {p.displayPrice} + ))} + + ); +}`} + ), + swift: ( + {`let products = try await OpenIapModule.shared.fetchProducts( + ProductRequest(skus: ["com.app.premium"], type: .inApp) +)`} + ), + kotlin: ( + {`val products = openIapStore.fetchProducts( + ProductRequest(skus = listOf("com.app.premium"), type = ProductQueryType.InApp) +)`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() + +val products = kmpIAP.fetchProducts( + ProductRequest(skus = listOf("com.app.premium"), type = ProductQueryType.InApp) +)`} + ), + dart: ( + {`final FetchProductsResult result = await FlutterInappPurchase.instance.fetchProducts( + skus: ['com.app.premium'], + type: ProductQueryType.InApp, +); + +// fetchProducts returns a sealed FetchProductsResult — unwrap by variant. +final List products = switch (result) { + FetchProductsResultProducts(value: final list) => list ?? [], + _ => [], +};`} + ), + gdscript: ( + {`var request = ProductRequest.new() +request.skus = ["com.app.coins_100", "com.app.premium"] +request.type = ProductQueryType.IN_APP +var products = await iap.fetch_products(request)`} + ), + }} + + +

    + See: Product,{' '} + ProductSubscription +

    +
    + ); +} + +export default FetchProducts; diff --git a/packages/docs/src/pages/docs/apis/finish-transaction.tsx b/packages/docs/src/pages/docs/apis/finish-transaction.tsx new file mode 100644 index 000000000..fe1cb88e8 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/finish-transaction.tsx @@ -0,0 +1,135 @@ +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function FinishTransaction() { + useScrollToHash(); + + return ( +
    + +

    finishTransaction

    +

    + Complete a purchase transaction. Must be called after verifying the + purchase to remove it from the queue. +

    + +

    Signature

    + + {{ + typescript: ( + {`finishTransaction(args: MutationFinishTransactionArgs): Promise + +interface MutationFinishTransactionArgs { + purchase: Purchase; + isConsumable?: boolean | null; +}`} + ), + swift: ( + {`func finishTransaction(_ purchase: Purchase, isConsumable: Bool = false) async throws`} + ), + kotlin: ( + {`suspend fun finishTransaction(purchase: Purchase, isConsumable: Boolean = false)`} + ), + kmp: ( + {`suspend fun finishTransaction(purchase: Purchase, isConsumable: Boolean = false)`} + ), + dart: ( + {`Future finishTransaction(Purchase purchase, {bool isConsumable = false});`} + ), + gdscript: ( + {`func finish_transaction(purchase: Purchase, is_consumable: bool = false) -> void`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { finishTransaction, purchaseUpdatedListener } from 'expo-iap'; +// Same API in react-native-iap: +// import { finishTransaction, purchaseUpdatedListener } from 'react-native-iap'; + +purchaseUpdatedListener(async (purchase) => { + const verified = await verifyOnServer(purchase); + if (!verified) return; + + await grantProduct(purchase.productId); + + const isConsumable = purchase.productId.includes('coins'); + await finishTransaction({ purchase, isConsumable }); +}); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP wires the purchase listener for you; finish the transaction inside +// the onPurchaseSuccess callback. +import { useIAP } from 'expo-iap'; + +function PurchaseScreen() { + const { finishTransaction } = useIAP({ + onPurchaseSuccess: async (purchase) => { + const verified = await verifyOnServer(purchase); + if (!verified) return; + + await grantProduct(purchase.productId); + const isConsumable = purchase.productId.includes('coins'); + await finishTransaction({ purchase, isConsumable }); + }, + }); + + return null; +}`} + ), + swift: ( + {`try await OpenIapModule.shared.finishTransaction(purchase, isConsumable: false)`} + ), + kotlin: ( + {`openIapStore.finishTransaction(purchase, isConsumable = false)`} + ), + kmp: ( + {`kmpIAP.finishTransaction(purchase, isConsumable = false) + +// --- Or via the DSL API --- +// requestPurchase { } returns a Purchase you pass through +// .toPurchaseInput() into finishTransaction. Use isConsumable = true for +// consumables, false for subscriptions / non-consumables. +val purchase = kmpIAP.requestPurchase { + ios { sku = "com.app.coins_100" } + android { skus = listOf("com.app.coins_100") } +} + +// After server-side validation: +kmpIAP.finishTransaction( + purchase = purchase.toPurchaseInput(), + isConsumable = true +)`} + ), + dart: ( + {`await FlutterInappPurchase.instance.finishTransaction(purchase);`} + ), + gdscript: ( + {`await iap.finish_transaction(purchase, false)`} + ), + }} + + +
    +

    + Critical: Android purchases must be acknowledged + within 3 days or they will be automatically refunded. iOS transactions + will replay on every app launch if not finished. +

    +
    +
    + ); +} + +export default FinishTransaction; diff --git a/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx new file mode 100644 index 000000000..b168f3bca --- /dev/null +++ b/packages/docs/src/pages/docs/apis/get-active-subscriptions.tsx @@ -0,0 +1,112 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function GetActiveSubscriptions() { + useScrollToHash(); + + return ( +
    + +

    getActiveSubscriptions

    +

    + Get all active subscriptions with detailed renewal status information. +

    + +

    Signature

    + + {{ + typescript: ( + {`getActiveSubscriptions(subscriptionIds?: string[]): Promise`} + ), + swift: ( + {`func getActiveSubscriptions(subscriptionIds: [String]? = nil) async throws -> [ActiveSubscription]`} + ), + kotlin: ( + {`suspend fun getActiveSubscriptions(subscriptionIds: List? = null): List`} + ), + kmp: ( + {`suspend fun getActiveSubscriptions(subscriptionIds: List? = null): List`} + ), + dart: ( + {`Future> getActiveSubscriptions({List? subscriptionIds});`} + ), + gdscript: ( + {`func get_active_subscriptions(subscription_ids: Array[String] = []) -> Array[ActiveSubscription]`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { getActiveSubscriptions } from 'expo-iap'; +// Same API in react-native-iap: +// import { getActiveSubscriptions } from 'react-native-iap'; + +const subscriptions = await getActiveSubscriptions(); + +for (const sub of subscriptions) { + console.log(\`Product: \${sub.productId}\`); + if (sub.renewalInfoIOS?.willAutoRenew === false) { + console.log('Subscription cancelled, will not renew'); + } +} + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP exposes getActiveSubscriptions plus a reactive activeSubscriptions +// list that is refreshed whenever the call resolves. +import { useIAP } from 'expo-iap'; + +function SubscriptionStatus() { + const { activeSubscriptions, getActiveSubscriptions } = useIAP(); + + useEffect(() => { + void getActiveSubscriptions(); + }, [getActiveSubscriptions]); + + return ( + + {activeSubscriptions.map((sub) => ( + {sub.productId} + ))} + + ); +}`} + ), + swift: ( + {`let subscriptions = try await OpenIapModule.shared.getActiveSubscriptions()`} + ), + kotlin: ( + {`val subscriptions = openIapStore.getActiveSubscriptions()`} + ), + kmp: ( + {`val subscriptions = kmpIAP.getActiveSubscriptions()`} + ), + dart: ( + {`final subscriptions = await FlutterInappPurchase.instance.getActiveSubscriptions();`} + ), + gdscript: ( + {`var subscriptions = await iap.get_active_subscriptions()`} + ), + }} + + +

    + See:{' '} + ActiveSubscription +

    +
    + ); +} + +export default GetActiveSubscriptions; diff --git a/packages/docs/src/pages/docs/apis/get-available-purchases.tsx b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx new file mode 100644 index 000000000..ab35df4a3 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/get-available-purchases.tsx @@ -0,0 +1,123 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function GetAvailablePurchases() { + useScrollToHash(); + + return ( +
    + +

    getAvailablePurchases

    +

    + Get all available (unfinished) purchases for the current user. Use this + to restore purchases or check for pending transactions. +

    + +

    Signature

    + + {{ + typescript: ( + {`getAvailablePurchases(options?: PurchaseOptions): Promise + +interface PurchaseOptions { + alsoPublishToEventListenerIOS?: boolean; + onlyIncludeActiveItemsIOS?: boolean; +}`} + ), + swift: ( + {`func getAvailablePurchases(options: PurchaseOptions? = nil) async throws -> [Purchase]`} + ), + kotlin: ( + {`suspend fun getAvailablePurchases(): List`} + ), + kmp: ( + {`suspend fun getAvailablePurchases(): List`} + ), + dart: ( + {`Future> getAvailablePurchases({PurchaseOptions? options});`} + ), + gdscript: ( + {`func get_available_purchases(options: PurchaseOptions = null) -> Array[Purchase]`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { getAvailablePurchases, finishTransaction } from 'expo-iap'; +// Same API in react-native-iap: +// import { getAvailablePurchases, finishTransaction } from 'react-native-iap'; + +const purchases = await getAvailablePurchases(); + +for (const purchase of purchases) { + const verified = await verifyOnServer(purchase); + if (verified) { + await finishTransaction({ purchase, isConsumable: false }); + } +} + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP's getAvailablePurchases() returns Promise and updates the +// reactive availablePurchases array — process new entries inside an effect. +import { useIAP } from 'expo-iap'; + +function PendingPurchases() { + const { availablePurchases, getAvailablePurchases, finishTransaction } = + useIAP(); + + useEffect(() => { + void getAvailablePurchases(); + }, [getAvailablePurchases]); + + useEffect(() => { + (async () => { + for (const purchase of availablePurchases) { + const verified = await verifyOnServer(purchase); + if (verified) { + await finishTransaction({ purchase, isConsumable: false }); + } + } + })(); + }, [availablePurchases, finishTransaction]); + + return null; +}`} + ), + swift: ( + {`let purchases = try await OpenIapModule.shared.getAvailablePurchases()`} + ), + kotlin: ( + {`val purchases = openIapStore.getAvailablePurchases()`} + ), + kmp: ( + {`val purchases = kmpIAP.getAvailablePurchases()`} + ), + dart: ( + {`final purchases = await FlutterInappPurchase.instance.getAvailablePurchases();`} + ), + gdscript: ( + {`var purchases = await iap.get_available_purchases()`} + ), + }} + + +

    + See: Purchase +

    +
    + ); +} + +export default GetAvailablePurchases; diff --git a/packages/docs/src/pages/docs/apis/get-storefront.tsx b/packages/docs/src/pages/docs/apis/get-storefront.tsx new file mode 100644 index 000000000..92df9add5 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/get-storefront.tsx @@ -0,0 +1,100 @@ +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function GetStorefront() { + useScrollToHash(); + + return ( +
    + +

    getStorefront

    +

    Get the storefront country code for the active user.

    + +

    Signature

    + + {{ + typescript: ( + {`getStorefront(): Promise`} + ), + swift: ( + {`func getStorefront() async throws -> String`} + ), + kotlin: ( + {`suspend fun getStorefront(): String`} + ), + kmp: ( + {`suspend fun getStorefront(): String`} + ), + dart: ( + {`Future getStorefront();`} + ), + gdscript: ( + {`func get_storefront() -> String`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { getStorefront } from 'expo-iap'; +// Same API in react-native-iap: +// import { getStorefront } from 'react-native-iap'; + +const countryCode = await getStorefront(); +console.log(countryCode); // "US", "JP", "GB", etc. + +// --- Or alongside the useIAP() hook (also exported from react-native-iap) --- +// getStorefront is a module-level helper; useIAP doesn't expose it on the +// hook return, so call the module function from inside your component once +// the hook reports the connection is ready. +import { useIAP } from 'expo-iap'; + +function StorefrontBadge() { + const { connected } = useIAP(); + const [country, setCountry] = useState(''); + + useEffect(() => { + if (!connected) return; + void getStorefront().then(setCountry); + }, [connected]); + + return Storefront: {country}; +}`} + ), + swift: ( + {`let countryCode = try await OpenIapModule.shared.getStorefront()`} + ), + kotlin: ( + {`val countryCode = openIapStore.getStorefront()`} + ), + kmp: ( + {`val countryCode = kmpIAP.getStorefront()`} + ), + dart: ( + {`final countryCode = await FlutterInappPurchase.instance.getStorefront();`} + ), + gdscript: ( + {`var country_code = await iap.get_storefront()`} + ), + }} + + +

    + Returns the ISO 3166-1 alpha-2 country code. Returns an empty string + when the storefront cannot be determined. +

    +
    + ); +} + +export default GetStorefront; diff --git a/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx b/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx new file mode 100644 index 000000000..e83245096 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/has-active-subscriptions.tsx @@ -0,0 +1,91 @@ +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function HasActiveSubscriptions() { + useScrollToHash(); + + return ( +
    + +

    hasActiveSubscriptions

    +

    Quick check if the user has any active subscriptions.

    + +

    Signature

    + + {{ + typescript: ( + {`hasActiveSubscriptions(subscriptionIds?: string[]): Promise`} + ), + swift: ( + {`func hasActiveSubscriptions(subscriptionIds: [String]? = nil) async throws -> Bool`} + ), + kotlin: ( + {`suspend fun hasActiveSubscriptions(subscriptionIds: List? = null): Boolean`} + ), + kmp: ( + {`suspend fun hasActiveSubscriptions(subscriptionIds: List? = null): Boolean`} + ), + dart: ( + {`Future hasActiveSubscriptions({List? subscriptionIds});`} + ), + gdscript: ( + {`func has_active_subscriptions(subscription_ids: Array[String] = []) -> bool`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { hasActiveSubscriptions } from 'expo-iap'; +// Same API in react-native-iap: +// import { hasActiveSubscriptions } from 'react-native-iap'; + +const isPremium = await hasActiveSubscriptions(); +const hasProPlan = await hasActiveSubscriptions(['pro_monthly', 'pro_yearly']); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +import { useIAP } from 'expo-iap'; + +function PremiumGate({ children }: { children: React.ReactNode }) { + const { hasActiveSubscriptions } = useIAP(); + const [isPremium, setIsPremium] = useState(false); + + useEffect(() => { + void hasActiveSubscriptions().then(setIsPremium); + }, [hasActiveSubscriptions]); + + return isPremium ? <>{children} : Subscribe to unlock; +}`} + ), + swift: ( + {`let isPremium = try await OpenIapModule.shared.hasActiveSubscriptions()`} + ), + kotlin: ( + {`val isPremium = openIapStore.hasActiveSubscriptions()`} + ), + kmp: ( + {`val isPremium = kmpIAP.hasActiveSubscriptions()`} + ), + dart: ( + {`final isPremium = await FlutterInappPurchase.instance.hasActiveSubscriptions();`} + ), + gdscript: ( + {`var is_premium = await iap.has_active_subscriptions()`} + ), + }} + +
    + ); +} + +export default HasActiveSubscriptions; diff --git a/packages/docs/src/pages/docs/apis/index.tsx b/packages/docs/src/pages/docs/apis/index.tsx index 001170a5d..ecdd48e72 100644 --- a/packages/docs/src/pages/docs/apis/index.tsx +++ b/packages/docs/src/pages/docs/apis/index.tsx @@ -1,101 +1,128 @@ import { useEffect } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; -import APICard from '../../../components/APICard'; import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; -// Redirect map for legacy anchor links -const legacyAnchorRedirects: Record = { - // Connection - 'init-connection': '/docs/apis/connection#init-connection', - 'end-connection': '/docs/apis/connection#end-connection', - // Products - 'fetch-products': '/docs/apis/products#fetch-products', - 'get-available-purchases': '/docs/apis/products#get-available-purchases', - // Purchase - 'request-purchase': '/docs/apis/purchase#request-purchase', - 'finish-transaction': '/docs/apis/purchase#finish-transaction', - 'restore-purchases': '/docs/apis/purchase#restore-purchases', - 'get-storefront': '/docs/apis/purchase#get-storefront', - // Subscription - 'get-active-subscriptions': - '/docs/apis/subscription#get-active-subscriptions', - 'has-active-subscriptions': - '/docs/apis/subscription#has-active-subscriptions', - 'deep-link-to-subscriptions': - '/docs/apis/subscription#deep-link-to-subscriptions', - // Validation - 'verify-purchase': '/docs/apis/validation#verify-purchase', - 'verify-purchase-with-provider': - '/docs/apis/validation#verify-purchase-with-provider', - 'purchase-identifier-usage': '/docs/apis/validation#purchase-identifiers', - // iOS Specific - 'clear-transaction-ios': '/docs/apis/ios#clear-transaction-ios', - 'get-storefront-ios': '/docs/apis/ios#get-storefront-ios', - 'get-promoted-product-ios': '/docs/apis/ios#get-promoted-product-ios', +// Old bookmarks pointed at /docs/apis#; map those to the flat +// per-symbol routes introduced in this PR so existing external links +// keep working. The slug → route mapping mirrors the routes in +// pages/docs/index.tsx; iOS / Android symbols redirect into their +// platform subfolders. +const LEGACY_ANCHOR_REDIRECTS: Record = { + 'init-connection': '/docs/apis/init-connection', + 'end-connection': '/docs/apis/end-connection', + 'fetch-products': '/docs/apis/fetch-products', + 'get-available-purchases': '/docs/apis/get-available-purchases', + 'request-purchase': '/docs/apis/request-purchase', + 'finish-transaction': '/docs/apis/finish-transaction', + 'restore-purchases': '/docs/apis/restore-purchases', + 'get-storefront': '/docs/apis/get-storefront', + 'get-active-subscriptions': '/docs/apis/get-active-subscriptions', + 'has-active-subscriptions': '/docs/apis/has-active-subscriptions', + 'deep-link-to-subscriptions': '/docs/apis/deep-link-to-subscriptions', + // iOS-specific + 'clear-transaction-ios': '/docs/apis/ios/clear-transaction-ios', + 'get-storefront-ios': '/docs/apis/ios/get-storefront-ios', + 'get-promoted-product-ios': '/docs/apis/ios/get-promoted-product-ios', 'request-purchase-on-promoted-product-ios': - '/docs/apis/ios#request-purchase-on-promoted-product-ios', - 'get-pending-transactions-ios': '/docs/apis/ios#get-pending-transactions-ios', + '/docs/apis/ios/request-purchase-on-promoted-product-ios', + 'get-pending-transactions-ios': '/docs/apis/ios/get-pending-transactions-ios', + 'get-all-transactions-ios': '/docs/apis/ios/get-all-transactions-ios', 'is-eligible-for-intro-offer-ios': - '/docs/apis/ios#is-eligible-for-intro-offer-ios', - 'subscription-status-ios': '/docs/apis/ios#subscription-status-ios', - 'current-entitlement-ios': '/docs/apis/ios#current-entitlement-ios', - 'latest-transaction-ios': '/docs/apis/ios#latest-transaction-ios', + '/docs/apis/ios/is-eligible-for-intro-offer-ios', + 'subscription-status-ios': '/docs/apis/ios/subscription-status-ios', + 'current-entitlement-ios': '/docs/apis/ios/current-entitlement-ios', + 'latest-transaction-ios': '/docs/apis/ios/latest-transaction-ios', 'show-manage-subscriptions-ios': - '/docs/apis/ios#show-manage-subscriptions-ios', - 'begin-refund-request-ios': '/docs/apis/ios#begin-refund-request-ios', - 'is-transaction-verified-ios': '/docs/apis/ios#is-transaction-verified-ios', - 'get-transaction-jws-ios': '/docs/apis/ios#get-transaction-jws-ios', - 'get-receipt-data-ios': '/docs/apis/ios#get-receipt-data-ios', - 'sync-ios': '/docs/apis/ios#sync-ios', + '/docs/apis/ios/show-manage-subscriptions-ios', + 'is-transaction-verified-ios': '/docs/apis/ios/is-transaction-verified-ios', + 'get-transaction-jws-ios': '/docs/apis/ios/get-transaction-jws-ios', + 'get-receipt-data-ios': '/docs/apis/ios/get-receipt-data-ios', + 'begin-refund-request-ios': '/docs/apis/ios/begin-refund-request-ios', 'present-code-redemption-sheet-ios': - '/docs/apis/ios#present-code-redemption-sheet-ios', - 'get-app-transaction-ios': '/docs/apis/ios#get-app-transaction-ios', + '/docs/apis/ios/present-code-redemption-sheet-ios', + 'get-app-transaction-ios': '/docs/apis/ios/get-app-transaction-ios', 'can-present-external-purchase-notice-ios': - '/docs/apis/ios#can-present-external-purchase-notice-ios', + '/docs/apis/ios/can-present-external-purchase-notice-ios', 'present-external-purchase-notice-sheet-ios': - '/docs/apis/ios#present-external-purchase-notice-sheet-ios', + '/docs/apis/ios/present-external-purchase-notice-sheet-ios', 'present-external-purchase-link-ios': - '/docs/apis/ios#present-external-purchase-link-ios', - 'validate-receipt-ios': '/docs/apis/ios#validate-receipt-ios', - // Android Specific + '/docs/apis/ios/present-external-purchase-link-ios', + 'is-eligible-for-external-purchase-custom-link-ios': + '/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios', + 'get-external-purchase-custom-link-token-ios': + '/docs/apis/ios/get-external-purchase-custom-link-token-ios', + 'show-external-purchase-custom-link-notice-ios': + '/docs/apis/ios/show-external-purchase-custom-link-notice-ios', + 'sync-ios': '/docs/apis/ios/sync-ios', + 'validate-receipt-ios': '/docs/apis/ios/validate-receipt-ios', + // Android-specific 'acknowledge-purchase-android': - '/docs/apis/android#acknowledge-purchase-android', - 'consume-purchase-android': '/docs/apis/android#consume-purchase-android', + '/docs/apis/android/acknowledge-purchase-android', + 'consume-purchase-android': '/docs/apis/android/consume-purchase-android', 'check-alternative-billing-availability-android': - '/docs/apis/android#check-alternative-billing-availability-android', + '/docs/apis/android/check-alternative-billing-availability-android', 'show-alternative-billing-dialog-android': - '/docs/apis/android#show-alternative-billing-dialog-android', + '/docs/apis/android/show-alternative-billing-dialog-android', 'create-alternative-billing-token-android': - '/docs/apis/android#create-alternative-billing-token-android', - // Legacy section anchors + '/docs/apis/android/create-alternative-billing-token-android', + 'enable-billing-program-android': + '/docs/apis/android/enable-billing-program-android', + 'is-billing-program-available-android': + '/docs/apis/android/is-billing-program-available-android', + 'launch-external-link-android': + '/docs/apis/android/launch-external-link-android', + 'create-billing-program-reporting-details-android': + '/docs/apis/android/create-billing-program-reporting-details-android', + // Validation/Refund/Debugging moved to Features + 'verify-purchase': '/docs/features/validation#verify-purchase', + 'verify-purchase-with-provider': + '/docs/features/validation#verify-purchase-with-provider', + 'validate-receipt': '/docs/features/validation#verify-purchase', + validation: '/docs/features/validation', + refund: '/docs/features/refund', + debugging: '/docs/features/debugging', + 'debugging-logging': '/docs/features/debugging', + // Section-level anchors that pointed at the old combined page + 'platform-specific-apis': '/docs/apis#ios-functions', + 'ios-apis': '/docs/apis#ios-functions', + 'android-apis': '/docs/apis#android-functions', terminology: '/docs/apis#terminology', - 'request-apis': '/docs/apis#request-apis', - 'connection-management': '/docs/apis/connection', - 'product-management': '/docs/apis/products', - 'purchase-operations': '/docs/apis/purchase', - 'subscription-management': '/docs/apis/subscription', - validation: '/docs/apis/validation', - 'platform-specific-apis': '/docs/apis/ios', - 'ios-apis': '/docs/apis/ios', - 'android-apis': '/docs/apis/android', + 'request-apis': '/docs/apis/fetch-products#request-apis', + 'naming-convention': '/docs/apis#naming-convention', }; function APIsIndex() { + useScrollToHash(); const location = useLocation(); const navigate = useNavigate(); - // Redirect legacy anchor links to new paths useEffect(() => { - const hash = location.hash.slice(1); // Remove '#' - if (hash && legacyAnchorRedirects[hash]) { - navigate(legacyAnchorRedirects[hash], { replace: true }); + if (!location.hash) return; + const anchor = location.hash.slice(1); + const redirect = LEGACY_ANCHOR_REDIRECTS[anchor]; + if (!redirect) return; + // Skip the navigate when the redirect target already matches the + // current pathname + hash. Same-page anchors (`terminology`, etc.) + // would otherwise re-fire this effect and infinite-loop, and even + // cross-page redirects would push a duplicate history entry on + // re-renders if the URL is already correct. + const [redirectPath, redirectHash = ''] = redirect.split('#'); + const currentHash = location.hash.startsWith('#') + ? location.hash.slice(1) + : ''; + // Normalise trailing slashes so `/foo` and `/foo/` compare equal. + const stripSlash = (p: string) => + p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p; + if ( + stripSlash(redirectPath) === stripSlash(location.pathname) && + redirectHash === currentHash + ) { + return; } - }, [location.hash, navigate]); - - useScrollToHash(); + navigate(redirect, { replace: true }); + }, [location.hash, location.pathname, navigate]); return (
    @@ -107,152 +134,615 @@ function APIsIndex() { />

    APIs

    - Complete API reference for OpenIAP. APIs are organized by functionality - to help you find what you need quickly. + Complete function reference for OpenIAP. Every public function is listed + below with a one-line description and a link to its full signature. For + higher-level guides see{' '} + Features.

    - -
      -
    • - - Connection - - : Initialize and manage store connection -
    • -
    • - - Products - - : Fetch product information -
    • -
    • - - Purchase - - : Request and complete purchases -
    • -
    • - - Subscription - - : Manage subscriptions -
    • -
    • - - Validation - - : Verify purchases server-side -
    • -
    • - - iOS Specific - {' '} - |{' '} - - Android Specific - -
    • -
    • - - Debugging - - : Error handling and troubleshooting -
    • -
    -
    +
    + + Connection + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + initConnection + + Initialize the store connection. Call before any IAP API.
    + + endConnection + + Close the store connection and release resources.
    +
    + +
    + + Products + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + fetchProducts + + Fetch products or subscriptions from the store.
    + + getAvailablePurchases + + List active purchases for the current user.
    +
    -

    Core APIs

    -

    Essential APIs used in every IAP implementation.

    -
    - - - - -
    + + Purchase + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + requestPurchase + + Initiate a purchase or subscription flow.
    + + finishTransaction + + + Complete a transaction after server-side verification. Required + on Android within 3 days. +
    + + restorePurchases + + Restore non-consumable and active subscription purchases.
    + + getStorefront + + Return the user's storefront country code.
    -

    Advanced APIs

    -

    Additional APIs for validation and debugging.

    -
    - - -
    + + Subscription + + + + + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + getActiveSubscriptions + + Get details of all currently active subscriptions.
    + + hasActiveSubscriptions + + Check whether the user has any active subscription.
    + + deepLinkToSubscriptions + + Open the platform's subscription management UI.
    -

    Platform-Specific APIs

    + + Validation +

    - APIs available only on specific platforms. Use these for - platform-specific features. + Server-side verification helpers. Full walkthrough lives on{' '} + Features → Validation — + these signatures are listed here for completeness.

    -
    - - -
    + + + + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + verifyPurchase + + + Verify a purchase against your own backend (returns{' '} + isValid + raw store metadata). +
    + + verifyPurchaseWithProvider + + + Verify via a managed provider (IAPKit, Apple, Google, Horizon) + without standing up your own server. +
    + + + validateReceipt + + + + Deprecated. Use{' '} + + verifyPurchase + {' '} + instead — same input/output shape. +
    -

    API Naming Convention

    -

    OpenIAP follows a consistent naming pattern:

    + + iOS Functions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + syncIOS + + Force sync transactions with the App Store.
    + + + getStorefrontIOS + + + + Deprecated. Use cross-platform{' '} + + getStorefront + {' '} + instead. +
    + + clearTransactionIOS + + Clear pending transactions in the queue (sandbox helper).
    + + getPromotedProductIOS + + Read the App Store-promoted product, if any.
    + + requestPurchaseOnPromotedProductIOS + + Buy the currently promoted product.
    + + getPendingTransactionsIOS + + List unfinished StoreKit transactions.
    + + getAllTransactionsIOS + + + List every StoreKit transaction (finished + unfinished) for the + current user. +
    + + isEligibleForIntroOfferIOS + + Check intro-offer eligibility for a subscription group.
    + + subscriptionStatusIOS + + Get subscription status objects from StoreKit 2.
    + + currentEntitlementIOS + + Get the user's current entitlement for a product.
    + + latestTransactionIOS + + Get the latest verified transaction for a product.
    + + showManageSubscriptionsIOS + + Present the manage-subscriptions sheet.
    + + beginRefundRequestIOS + + + Present the refund request sheet (iOS 15+). See{' '} + Refund. +
    + + isTransactionVerifiedIOS + + Check whether a transaction's JWS verification passed.
    + + getTransactionJwsIOS + + Return the JWS string for a transaction.
    + + getReceiptDataIOS + + Get base64 receipt data (legacy validation).
    + + presentCodeRedemptionSheetIOS + + + Show the App Store offer code redemption sheet. See{' '} + + Offer Code Redemption + + . +
    + + getAppTransactionIOS + + Fetch the app transaction (iOS 16+).
    + + canPresentExternalPurchaseNoticeIOS + + + Check eligibility for the external purchase notice sheet (iOS + 17.4+). +
    + + presentExternalPurchaseNoticeSheetIOS + + Present the external purchase notice sheet (iOS 17.4+).
    + + presentExternalPurchaseLinkIOS + + + Present an external purchase link, StoreKit External (iOS 16+). +
    + + isEligibleForExternalPurchaseCustomLinkIOS + + + Check eligibility for the custom-link variant of external + purchase (iOS 18.1+). +
    + + getExternalPurchaseCustomLinkTokenIOS + + + Fetch a token for Apple's External Purchase Server reporting API + (iOS 18.1+). +
    + + showExternalPurchaseCustomLinkNoticeIOS + + + Present the disclosure sheet required before linking out via + ExternalPurchaseCustomLink (iOS 18.1+). +
    + + + validateReceiptIOS + + + + Deprecated. Legacy App Store receipt + validation. Use{' '} + + verifyPurchase + {' '} + instead. +
    +
    + +
    + + Android Functions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FunctionDescription
    + + acknowledgePurchaseAndroid + + + Acknowledge a non-consumable purchase. Required within 3 days or + Google auto-refunds. +
    + + consumePurchaseAndroid + + Consume a consumable purchase so it can be re-bought.
    + + checkAlternativeBillingAvailabilityAndroid + + + Check whether alternative billing is available for the user. +
    + + showAlternativeBillingDialogAndroid + + Display Google's alternative billing information dialog.
    + + createAlternativeBillingTokenAndroid + + Create a reporting token for an alternative billing flow.
    + + enableBillingProgramAndroid + + + Enable a Play Billing Program (Play Billing 8.2.0+). Note: this + is a config field of{' '} + + InitConnectionConfig + {' '} + passed to initConnection(), not a standalone + mutation; the page documents the config-flow shape. +
    + + isBillingProgramAvailableAndroid + + + Check whether a billing program (e.g., External Payments) is + available for the current user. +
    + + launchExternalLinkAndroid + + + Launch an external content / offer link from inside the Billing + Programs flow (Play Billing 8.2.0+). +
    + + createBillingProgramReportingDetailsAndroid + + + Create the reporting payload Google requires after a + Developer-Provided Billing transaction (Play Billing 8.3.0+). +
    +
    + +
    + + Naming Convention +
    • - Cross-platform APIs: No suffix (e.g.,{' '} - fetchProducts, requestPurchase) + Cross-platform: no suffix (e.g.,{' '} + fetchProducts, requestPurchase).
    • - iOS-only APIs: End with IOS (e.g.,{' '} - syncIOS, getStorefrontIOS) + iOS-only: ends with IOS (e.g.,{' '} + syncIOS).
    • - Android-only APIs: End with Android{' '} - (e.g., acknowledgePurchaseAndroid) + Android-only: ends with Android (e.g.,{' '} + acknowledgePurchaseAndroid).

    - See: Type Definitions for complete type - information. + See: Type Definitions.

    @@ -260,46 +750,6 @@ function APIsIndex() { Terminology - - Request APIs - -
    -

    - Important: APIs starting with request{' '} - are event-based operations, not promise-based. -

    -

    - While these APIs return values for various purposes, you should{' '} - - not rely on their return values for actual purchase results - - . Instead, listen for events through{' '} - purchaseUpdatedListener or{' '} - purchaseErrorListener. -

    -

    - This is because Apple's purchase system is fundamentally - event-based, not promise-based. For more details, see this{' '} - - issue comment - - . -

    -

    - The request prefix indicates that these are event - requests - use the appropriate listeners to handle the actual - results. -

    -
    -

    - See: Events for setting up purchase - listeners. -

    - Transaction vs Purchase @@ -313,13 +763,18 @@ function APIsIndex() {
  • Android (Google Play Billing): Uses{' '} - Purchase + + Purchase +
  • - OpenIAP normalizes this to Purchase in cross-platform - APIs for consistency, while platform-specific APIs may use the native - terminology. + OpenIAP normalizes this to{' '} + + Purchase + {' '} + in cross-platform APIs for consistency, while platform-specific APIs + may use the native terminology.

    diff --git a/packages/docs/src/pages/docs/apis/init-connection.tsx b/packages/docs/src/pages/docs/apis/init-connection.tsx new file mode 100644 index 000000000..7c17a4156 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/init-connection.tsx @@ -0,0 +1,145 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function InitConnection() { + useScrollToHash(); + + return ( +
    + +

    initConnection

    +

    + Initialize connection to the store service. Must be called before any + other IAP operations. +

    + +

    Signature

    + + {{ + typescript: ( + {`initConnection(config?: InitConnectionConfig): Promise`} + ), + swift: ( + {`func initConnection() async throws -> Bool`} + ), + kotlin: ( + {`suspend fun initConnection(config: InitConnectionConfig? = null): Boolean`} + ), + kmp: ( + {`suspend fun initConnection(config: InitConnectionConfig? = null): Boolean`} + ), + dart: ( + {`Future initConnection({InitConnectionConfig? config});`} + ), + gdscript: ( + {`func init_connection(config: InitConnectionConfig = null) -> bool`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { initConnection } from 'expo-iap'; +// Same API in react-native-iap: +// import { initConnection } from 'react-native-iap'; + +// Standard connection +await initConnection(); + +// Android with a billing program (preferred — see InitConnectionConfig) +await initConnection({ + enableBillingProgramAndroid: 'external-offer', +}); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP auto-connects on mount and disconnects on unmount, so you almost +// never need to call initConnection() yourself. Pass connection options +// (e.g. enableBillingProgramAndroid) to the hook directly, and read the +// reactive "connected" flag from its return value. +import { useIAP } from 'expo-iap'; + +function PurchaseScreen() { + const { connected } = useIAP({ + enableBillingProgramAndroid: 'external-offer', + }); + + return Store ready: {String(connected)}; +}`} + ), + swift: ( + {`import OpenIap + +try await OpenIapModule.shared.initConnection()`} + ), + kotlin: ( + {`// Standard connection +openIapStore.initConnection() + +// With alternative billing +openIapStore.initConnection( + InitConnectionConfig( + alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice + ) +)`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() + +// Standard connection +kmpIAP.initConnection() + +// With alternative billing +kmpIAP.initConnection( + InitConnectionConfig( + alternativeBillingModeAndroid = AlternativeBillingModeAndroid.UserChoice + ) +)`} + ), + dart: ( + {`await FlutterInappPurchase.instance.initConnection();`} + ), + gdscript: ( + {`# Standard connection +var success = await iap.init_connection() + +# With alternative billing (Android) +var config = InitConnectionConfig.new() +config.alternative_billing_mode_android = AlternativeBillingModeAndroid.USER_CHOICE +var success = await iap.init_connection(config)`} + ), + }} + + +

    + See{' '} + + InitConnectionConfig + {' '} + for the full list of supported config fields ( + + alternativeBillingModeAndroid + {' '} + [deprecated],{' '} + + enableBillingProgramAndroid + + ). +

    +
    + ); +} + +export default InitConnection; diff --git a/packages/docs/src/pages/docs/apis/ios.tsx b/packages/docs/src/pages/docs/apis/ios.tsx deleted file mode 100644 index 2795b7b53..000000000 --- a/packages/docs/src/pages/docs/apis/ios.tsx +++ /dev/null @@ -1,346 +0,0 @@ -import { Link } from 'react-router-dom'; -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function IOSAPIs() { - useScrollToHash(); - - return ( -
    - -

    iOS Specific

    -

    - iOS-specific APIs using StoreKit 2. These APIs are only available on - iOS/macOS and end with the IOS suffix. -

    - - - - - -
    - - Transaction Management - - - - clearTransactionIOS - -

    Clear pending transactions from the StoreKit payment queue.

    - {`func clearTransactionIOS() async throws -> Bool`} - - - getPendingTransactionsIOS - -

    Retrieve all pending transactions in the StoreKit queue.

    - {`func getPendingTransactionsIOS() async throws -> [Purchase]`} - - - getAllTransactionsIOS - -

    - Get the full StoreKit 2 transaction history as PurchaseIOS values. - Requires the SK2ConsumableTransactionHistory Info.plist key for - finished consumables to be included (iOS 18+). -

    - {`func getAllTransactionsIOS() async throws -> [PurchaseIOS]`} - - - syncIOS - -

    Force a StoreKit sync for transactions (iOS 15+).

    - {`func syncIOS() async throws -> Bool`} -
    - -
    - - Storefront & Products - - - - - getStorefrontIOS - {' '} - - (deprecated) - - -

    - Deprecated. Use{' '} - getStorefront(){' '} - instead. -

    - {`@available(*, deprecated, message: "Use getStorefront()") -func getStorefrontIOS() async throws -> String`} - - - getPromotedProductIOS - -

    Get the currently promoted product from App Store (iOS 11+).

    - {`func getPromotedProductIOS() async throws -> Product?`} -
    - -
    - - Subscription APIs - - - - isEligibleForIntroOfferIOS - -

    - Check introductory offer eligibility for a subscription group (iOS - 12.2+). -

    - {`func isEligibleForIntroOfferIOS(groupID: String) async throws -> Bool`} - - - subscriptionStatusIOS - -

    Get detailed subscription status using StoreKit 2 (iOS 15+).

    - {`func subscriptionStatusIOS(sku: String) async throws -> [SubscriptionStatus]`} - - - currentEntitlementIOS - -

    Get current StoreKit 2 entitlement for a product (iOS 15+).

    - {`func currentEntitlementIOS(sku: String) async throws -> Purchase?`} - - - latestTransactionIOS - -

    Get the most recent transaction for a product (iOS 15+).

    - {`func latestTransactionIOS(sku: String) async throws -> Purchase?`} - - - showManageSubscriptionsIOS - -

    - Show in-app subscription management UI and detect status changes (iOS - 15+). -

    - {`func showManageSubscriptionsIOS() async throws -> [Purchase]`} -

    - Returns purchases for subscriptions whose auto-renewal status changed. -

    -
    - -
    - - Verification - - - - isTransactionVerifiedIOS - -

    Verify a StoreKit 2 transaction signature (iOS 15+).

    - {`func isTransactionVerifiedIOS(sku: String) async throws -> Bool`} - - - getTransactionJwsIOS - -

    Get the transaction JWS for server-side validation (iOS 15+).

    - {`func getTransactionJwsIOS(sku: String) async throws -> String?`} - - - getReceiptDataIOS - -

    Get base64-encoded receipt data for legacy validation.

    - {`func getReceiptDataIOS() async throws -> String?`} -
    - -
    - - Refunds & Redemption - - - - beginRefundRequestIOS - -

    Initiate a refund request for a product (iOS 15+).

    - {`func beginRefundRequestIOS(sku: String) async throws -> String?`} - - - presentCodeRedemptionSheetIOS - -

    Present the App Store promo code redemption sheet.

    - {`func presentCodeRedemptionSheetIOS() async throws -> Bool`} - - - getAppTransactionIOS - -

    Fetch the current app transaction (iOS 16+).

    - {`func getAppTransactionIOS() async throws -> AppTransaction? - -struct AppTransaction { - let bundleId: String - let appVersion: String - let originalAppVersion: String - let originalPurchaseDate: Date - let environment: String // "Sandbox" | "Production" - // iOS 18.4+ properties - let appTransactionId: String? - let originalPlatform: String? -}`} -
    - -
    - - External Purchase (iOS 17.4+) - -

    - iOS supports external purchase links via Apple's{' '} - ExternalPurchase API. This requires a 3-step flow for - Apple compliance. -

    - - - canPresentExternalPurchaseNoticeIOS - -

    - Check if external purchase notice sheet can be presented (iOS 17.4+). -

    - {`func canPresentExternalPurchaseNoticeIOS() async throws -> Bool`} - - - presentExternalPurchaseNoticeSheetIOS - -

    Present Apple's compliance notice sheet (iOS 17.4+).

    - {`func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS - -struct ExternalPurchaseNoticeResultIOS { - let error: String? - let result: ExternalPurchaseNoticeAction // .continue or .dismissed -}`} - - - presentExternalPurchaseLinkIOS - -

    Open external purchase URL in Safari (iOS 18.2+).

    - {`func presentExternalPurchaseLinkIOS(_ url: String) async throws -> ExternalPurchaseLinkResultIOS - -struct ExternalPurchaseLinkResultIOS { - let error: String? - let success: Bool -}`} - -

    External Purchase Flow Example

    - {`// Step 1: Check availability -let canPresent = try await OpenIapModule.shared.canPresentExternalPurchaseNoticeIOS() -guard canPresent else { return } - -// Step 2: Show Apple's notice sheet -let noticeResult = try await OpenIapModule.shared.presentExternalPurchaseNoticeSheetIOS() -guard noticeResult.result == .continue else { return } - -// Step 3: Open external purchase link (iOS 18.2+) -let result = try await OpenIapModule.shared.presentExternalPurchaseLinkIOS( - "https://your-payment-site.com/checkout" -)`} - -
    -

    - Requirements: iOS 17.4+ for notice sheet, iOS 18.2+ - for custom links. App must have StoreKit external purchase - entitlement. -

    -
    -
    - -
    - - Deprecated APIs - - - - - requestPurchaseOnPromotedProductIOS - {' '} - - (deprecated) - - -

    - Deprecated. Use{' '} - - promotedProductListenerIOS - {' '} - to receive the product ID, then call{' '} - requestPurchase{' '} - with that SKU instead. -

    - {`@available(*, deprecated, message: "Use promotedProductListenerIOS + requestPurchase instead") -func requestPurchaseOnPromotedProductIOS() async throws -> Bool`} -

    - In StoreKit 2, promoted products can be purchased directly via the - standard purchase flow. When a user taps a promoted product in the App - Store, the promotedProductListenerIOS event fires with - the product ID. Use this ID to call requestPurchase(){' '} - directly. -

    - {`// Recommended approach -let subscription = promotedProductListenerIOS { productId in - // Call requestPurchase with the received productId - try await requestPurchase(RequestPurchaseProps( - request: .purchase(RequestPurchasePropsByPlatforms( - apple: RequestPurchaseIosProps(sku: productId) - )), - type: .inApp - )) -}`} - - - - validateReceiptIOS - {' '} - - (deprecated) - - -

    - Deprecated. Use{' '} - verifyPurchase{' '} - instead. -

    - {`@available(*, deprecated, message: "Use verifyPurchase()") -func validateReceiptIOS(options: PurchaseVerificationProps) async throws -> PurchaseVerificationResult`} -
    -
    - ); -} - -export default IOSAPIs; diff --git a/packages/docs/src/pages/docs/apis/ios/begin-refund-request-ios.tsx b/packages/docs/src/pages/docs/apis/ios/begin-refund-request-ios.tsx new file mode 100644 index 000000000..fa812d6f0 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/begin-refund-request-ios.tsx @@ -0,0 +1,43 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function BeginRefundRequestIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + beginRefundRequestIOS +

    +

    + Initiate a refund request for a product (iOS 15+). Presents the StoreKit + refund sheet. +

    + +

    Signature

    + + {{ + swift: ( + {`func beginRefundRequestIOS(sku: String) async throws -> String?`} + ), + }} + + +

    + See: Refund Guide +

    +
    + ); +} + +export default BeginRefundRequestIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx b/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx new file mode 100644 index 000000000..4f62def5e --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/can-present-external-purchase-notice-ios.tsx @@ -0,0 +1,37 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function CanPresentExternalPurchaseNoticeIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + canPresentExternalPurchaseNoticeIOS +

    +

    + Check if external purchase notice sheet can be presented (iOS 17.4+). +

    + +

    Signature

    + + {{ + swift: ( + {`func canPresentExternalPurchaseNoticeIOS() async throws -> Bool`} + ), + }} + +
    + ); +} + +export default CanPresentExternalPurchaseNoticeIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/clear-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/clear-transaction-ios.tsx new file mode 100644 index 000000000..632179e32 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/clear-transaction-ios.tsx @@ -0,0 +1,35 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function ClearTransactionIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + clearTransactionIOS +

    +

    Clear pending transactions from the StoreKit payment queue.

    + +

    Signature

    + + {{ + swift: ( + {`func clearTransactionIOS() async throws -> Bool`} + ), + }} + +
    + ); +} + +export default ClearTransactionIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx b/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx new file mode 100644 index 000000000..7d0834a7b --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/current-entitlement-ios.tsx @@ -0,0 +1,35 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function CurrentEntitlementIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + currentEntitlementIOS +

    +

    Get current StoreKit 2 entitlement for a product (iOS 15+).

    + +

    Signature

    + + {{ + swift: ( + {`func currentEntitlementIOS(sku: String) async throws -> Purchase?`} + ), + }} + +
    + ); +} + +export default CurrentEntitlementIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx new file mode 100644 index 000000000..b236cd207 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-all-transactions-ios.tsx @@ -0,0 +1,39 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function GetAllTransactionsIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getAllTransactionsIOS +

    +

    + Get the full StoreKit 2 transaction history as PurchaseIOS values. + Requires the SK2ConsumableTransactionHistory Info.plist key for finished + consumables to be included (iOS 18+). +

    + +

    Signature

    + + {{ + swift: ( + {`func getAllTransactionsIOS() async throws -> [PurchaseIOS]`} + ), + }} + +
    + ); +} + +export default GetAllTransactionsIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx new file mode 100644 index 000000000..91d8c97b1 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-app-transaction-ios.tsx @@ -0,0 +1,50 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function GetAppTransactionIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getAppTransactionIOS +

    +

    Fetch the current app transaction (iOS 16+).

    + +

    Signature

    + + {{ + swift: ( + {`func getAppTransactionIOS() async throws -> AppTransactionIOS?`} + ), + }} + + +

    + See:{' '} + + AppTransactionIOS + {' '} + for the full field reference (bundleId,{' '} + appVersion, originalAppVersion,{' '} + originalPurchaseDate, environment,{' '} + deviceVerification, deviceVerificationNonce,{' '} + signedDate, appId, appVersionId,{' '} + preorderDate, plus iOS 18.4+ additions like{' '} + appTransactionId and originalPlatform). +

    +
    + ); +} + +export default GetAppTransactionIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx new file mode 100644 index 000000000..51124119f --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-external-purchase-custom-link-token-ios.tsx @@ -0,0 +1,57 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function GetExternalPurchaseCustomLinkTokenIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getExternalPurchaseCustomLinkTokenIOS +

    +

    + Fetch an external-purchase token for the{' '} + + ExternalPurchaseCustomLink + {' '} + API (iOS 18.1+). Pair the returned token with Apple's External Purchase + Server API to report acquisition or services transactions. +

    + +

    Signature

    + + {{ + swift: ( + {`func getExternalPurchaseCustomLinkTokenIOS( + tokenType: ExternalPurchaseCustomLinkTokenTypeIOS +) async throws -> ExternalPurchaseCustomLinkTokenResultIOS`} + ), + }} + + +

    + tokenType is{' '} + ExternalPurchaseCustomLinkTokenTypeIOS.acquisition for new + customers or{' '} + ExternalPurchaseCustomLinkTokenTypeIOS.services for + existing ones. The result wraps the opaque token plus expiration + metadata. +

    +
    + ); +} + +export default GetExternalPurchaseCustomLinkTokenIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx new file mode 100644 index 000000000..25cc08985 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-pending-transactions-ios.tsx @@ -0,0 +1,35 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function GetPendingTransactionsIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getPendingTransactionsIOS +

    +

    Retrieve all pending transactions in the StoreKit queue.

    + +

    Signature

    + + {{ + swift: ( + {`func getPendingTransactionsIOS() async throws -> [Purchase]`} + ), + }} + +
    + ); +} + +export default GetPendingTransactionsIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx new file mode 100644 index 000000000..38a8070ab --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-promoted-product-ios.tsx @@ -0,0 +1,35 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function GetPromotedProductIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getPromotedProductIOS +

    +

    Get the currently promoted product from App Store (iOS 11+).

    + +

    Signature

    + + {{ + swift: ( + {`func getPromotedProductIOS() async throws -> Product?`} + ), + }} + +
    + ); +} + +export default GetPromotedProductIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx new file mode 100644 index 000000000..05b39119e --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-receipt-data-ios.tsx @@ -0,0 +1,112 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function GetReceiptDataIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getReceiptDataIOS +

    +

    Get base64-encoded receipt data for legacy validation.

    + +

    Signature

    + + {{ + typescript: ( + {`getReceiptDataIOS(): Promise`} + ), + swift: ( + {`func getReceiptDataIOS() async throws -> String?`} + ), + kotlin: ( + {`suspend fun getReceiptDataIOS(): String?`} + ), + kmp: ( + {`suspend fun getReceiptDataIOS(): String?`} + ), + dart: ( + {`Future getReceiptDataIOS();`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { getReceiptDataIOS } from 'expo-iap'; +// Same API in react-native-iap: +// import { getReceiptDataIOS } from 'react-native-iap'; + +const receipt = await getReceiptDataIOS(); +// Send the base64-encoded receipt to your server for legacy verifyReceipt. +console.log(receipt?.length ?? 0, 'bytes'); + +// --- Or alongside the useIAP() hook (also exported from react-native-iap) --- +// getReceiptDataIOS is a module-level helper; useIAP doesn't expose it on the +// hook return, so call the module function from inside your component once +// the hook reports the connection is ready. +import { useIAP } from 'expo-iap'; + +function ReceiptUploader() { + const { connected } = useIAP(); + + const upload = async () => { + if (!connected) return; + const receipt = await getReceiptDataIOS(); + if (!receipt) return; + await fetch('/api/validate-receipt', { + method: 'POST', + body: JSON.stringify({ receipt }), + }); + }; + + return
    + ); +} + +export default GetReceiptDataIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx new file mode 100644 index 000000000..1bd3f26e7 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-storefront-ios.tsx @@ -0,0 +1,44 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function GetStorefrontIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getStorefrontIOS +

    +

    Deprecated. Use getStorefront() (cross-platform) instead.

    + +
    +

    + Deprecated. Use the cross-platform API. Use{' '} + getStorefront instead. +

    +
    + +

    Signature

    + + {{ + swift: ( + {`@available(*, deprecated, message: "Use getStorefront()") +func getStorefrontIOS() async throws -> String`} + ), + }} + +
    + ); +} + +export default GetStorefrontIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx b/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx new file mode 100644 index 000000000..520f243a5 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/get-transaction-jws-ios.tsx @@ -0,0 +1,118 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function GetTransactionJwsIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + getTransactionJwsIOS +

    +

    Get the transaction JWS for server-side validation (iOS 15+).

    + +

    Signature

    + + {{ + typescript: ( + {`getTransactionJwsIOS(sku: string): Promise`} + ), + swift: ( + {`func getTransactionJwsIOS(sku: String) async throws -> String?`} + ), + kotlin: ( + {`suspend fun getTransactionJwsIOS(sku: String): String?`} + ), + kmp: ( + {`suspend fun getTransactionJwsIOS(sku: String): String?`} + ), + dart: ( + {`Future getTransactionJwsIOS(String sku);`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { getTransactionJwsIOS } from 'expo-iap'; +// Same API in react-native-iap: +// import { getTransactionJwsIOS } from 'react-native-iap'; + +const jws = await getTransactionJwsIOS('com.example.premium'); +if (jws) { + // Send the JWS to your server; verify it with Apple's public keys. + await fetch('/api/verify-transaction', { + method: 'POST', + body: JSON.stringify({ jws }), + }); +} + +// --- Or alongside the useIAP() hook (also exported from react-native-iap) --- +// getTransactionJwsIOS is a module-level helper; useIAP doesn't expose it on +// the hook return, so call the module function from inside your component. +import { useIAP } from 'expo-iap'; + +function ServerValidateButton({ sku }: { sku: string }) { + const { connected } = useIAP(); + + const validate = async () => { + if (!connected) return; + const jws = await getTransactionJwsIOS(sku); + if (!jws) return; + await api.verify(jws); + }; + + return
    + ); +} + +export default GetTransactionJwsIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx new file mode 100644 index 000000000..3f6d6594f --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-external-purchase-custom-link-ios.tsx @@ -0,0 +1,47 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function IsEligibleForExternalPurchaseCustomLinkIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + isEligibleForExternalPurchaseCustomLinkIOS +

    +

    + Check whether the app is eligible to use the{' '} + + ExternalPurchaseCustomLink + {' '} + API (iOS 18.1+). Returns true when the bundle is approved + for the corresponding entitlement and music-streaming-app-style flows + are allowed. +

    + +

    Signature

    + + {{ + swift: ( + {`func isEligibleForExternalPurchaseCustomLinkIOS() async throws -> Bool`} + ), + }} + +
    + ); +} + +export default IsEligibleForExternalPurchaseCustomLinkIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx new file mode 100644 index 000000000..0bc4e4fef --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/is-eligible-for-intro-offer-ios.tsx @@ -0,0 +1,38 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function IsEligibleForIntroOfferIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + isEligibleForIntroOfferIOS +

    +

    + Check introductory offer eligibility for a subscription group (iOS + 12.2+). +

    + +

    Signature

    + + {{ + swift: ( + {`func isEligibleForIntroOfferIOS(groupID: String) async throws -> Bool`} + ), + }} + +
    + ); +} + +export default IsEligibleForIntroOfferIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx b/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx new file mode 100644 index 000000000..04617fa13 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/is-transaction-verified-ios.tsx @@ -0,0 +1,106 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function IsTransactionVerifiedIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + isTransactionVerifiedIOS +

    +

    Verify a StoreKit 2 transaction signature (iOS 15+).

    + +

    Signature

    + + {{ + typescript: ( + {`isTransactionVerifiedIOS(sku: string): Promise`} + ), + swift: ( + {`func isTransactionVerifiedIOS(sku: String) async throws -> Bool`} + ), + kotlin: ( + {`suspend fun isTransactionVerifiedIOS(sku: String): Boolean`} + ), + kmp: ( + {`suspend fun isTransactionVerifiedIOS(sku: String): Boolean`} + ), + dart: ( + {`Future isTransactionVerifiedIOS(String sku);`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { isTransactionVerifiedIOS } from 'expo-iap'; +// Same API in react-native-iap: +// import { isTransactionVerifiedIOS } from 'react-native-iap'; + +const verified = await isTransactionVerifiedIOS('com.example.premium'); +if (!verified) { + // StoreKit 2 reported the JWS signature as unverified - don't grant entitlement. + return; +} + +// --- Or alongside the useIAP() hook (also exported from react-native-iap) --- +// isTransactionVerifiedIOS is a module-level helper; useIAP doesn't expose it +// on the hook return, so call the module function from inside your component. +import { useIAP } from 'expo-iap'; + +function VerifyButton({ sku }: { sku: string }) { + const { connected } = useIAP(); + + const verify = async () => { + if (!connected) return; + const ok = await isTransactionVerifiedIOS(sku); + Alert.alert(ok ? 'Verified' : 'Verification failed'); + }; + + return
    + ); +} + +export default IsTransactionVerifiedIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx b/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx new file mode 100644 index 000000000..1e96ec053 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/latest-transaction-ios.tsx @@ -0,0 +1,35 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function LatestTransactionIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + latestTransactionIOS +

    +

    Get the most recent transaction for a product (iOS 15+).

    + +

    Signature

    + + {{ + swift: ( + {`func latestTransactionIOS(sku: String) async throws -> Purchase?`} + ), + }} + +
    + ); +} + +export default LatestTransactionIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx b/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx new file mode 100644 index 000000000..6feaff794 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/present-code-redemption-sheet-ios.tsx @@ -0,0 +1,43 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function PresentCodeRedemptionSheetIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + presentCodeRedemptionSheetIOS +

    +

    Present the App Store promo code redemption sheet.

    + +

    Signature

    + + {{ + swift: ( + {`func presentCodeRedemptionSheetIOS() async throws -> Bool`} + ), + }} + + +

    + See:{' '} + + Offer Code Redemption + +

    +
    + ); +} + +export default PresentCodeRedemptionSheetIOS; 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 new file mode 100644 index 000000000..74e4f15df --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-link-ios.tsx @@ -0,0 +1,40 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function PresentExternalPurchaseLinkIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + presentExternalPurchaseLinkIOS +

    +

    Open external purchase URL in Safari (iOS 18.2+).

    + +

    Signature

    + + {{ + swift: ( + {`func presentExternalPurchaseLinkIOS(_ url: String) async throws -> ExternalPurchaseLinkResultIOS + +struct ExternalPurchaseLinkResultIOS { + let error: String? + let success: Bool +}`} + ), + }} + +
    + ); +} + +export default PresentExternalPurchaseLinkIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-ios.tsx b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-ios.tsx new file mode 100644 index 000000000..0b98c30c2 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/present-external-purchase-notice-sheet-ios.tsx @@ -0,0 +1,41 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function PresentExternalPurchaseNoticeSheetIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + presentExternalPurchaseNoticeSheetIOS +

    +

    Present Apple's compliance notice sheet (iOS 17.4+).

    + +

    Signature

    + + {{ + swift: ( + {`func presentExternalPurchaseNoticeSheetIOS() async throws -> ExternalPurchaseNoticeResultIOS + +struct ExternalPurchaseNoticeResultIOS { + let result: ExternalPurchaseNoticeAction + let error: String? + let externalPurchaseToken: String? +}`} + ), + }} + +
    + ); +} + +export default PresentExternalPurchaseNoticeSheetIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/request-purchase-on-promoted-product-ios.tsx b/packages/docs/src/pages/docs/apis/ios/request-purchase-on-promoted-product-ios.tsx new file mode 100644 index 000000000..2886ea4f3 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/request-purchase-on-promoted-product-ios.tsx @@ -0,0 +1,48 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function RequestPurchaseOnPromotedProductIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + requestPurchaseOnPromotedProductIOS +

    +

    + Deprecated. Use promotedProductListenerIOS plus requestPurchase instead. +

    + +
    +

    + Deprecated. In StoreKit 2, promoted products fire + promotedProductListenerIOS with the productId — call requestPurchase + with that SKU. Use{' '} + requestPurchase instead. +

    +
    + +

    Signature

    + + {{ + swift: ( + {`@available(*, deprecated, message: "Use promotedProductListenerIOS + requestPurchase instead") +func requestPurchaseOnPromotedProductIOS() async throws -> Bool`} + ), + }} + +
    + ); +} + +export default RequestPurchaseOnPromotedProductIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/show-external-purchase-custom-link-notice-ios.tsx b/packages/docs/src/pages/docs/apis/ios/show-external-purchase-custom-link-notice-ios.tsx new file mode 100644 index 000000000..86858c78a --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/show-external-purchase-custom-link-notice-ios.tsx @@ -0,0 +1,55 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function ShowExternalPurchaseCustomLinkNoticeIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + showExternalPurchaseCustomLinkNoticeIOS +

    +

    + Display the system disclosure notice for{' '} + + ExternalPurchaseCustomLink + {' '} + (iOS 18.1+). Apple requires this sheet to be presented after a + deliberate customer interaction, before you can route the user to an + external purchase URL. +

    + +

    Signature

    + + {{ + swift: ( + {`func showExternalPurchaseCustomLinkNoticeIOS( + noticeType: ExternalPurchaseCustomLinkNoticeTypeIOS +) async throws -> ExternalPurchaseCustomLinkNoticeResultIOS`} + ), + }} + + +

    + noticeType picks the disclosure style required by the flow + you are entering (e.g. .acquisition for first-time + payments, .services for ongoing services). +

    +
    + ); +} + +export default ShowExternalPurchaseCustomLinkNoticeIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx b/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx new file mode 100644 index 000000000..eda7db092 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/show-manage-subscriptions-ios.tsx @@ -0,0 +1,39 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function ShowManageSubscriptionsIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + showManageSubscriptionsIOS +

    +

    + Show in-app subscription management UI and detect status changes (iOS + 15+). Returns purchases for subscriptions whose auto-renewal status + changed. +

    + +

    Signature

    + + {{ + swift: ( + {`func showManageSubscriptionsIOS() async throws -> [Purchase]`} + ), + }} + +
    + ); +} + +export default ShowManageSubscriptionsIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx b/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx new file mode 100644 index 000000000..5d6fb7f89 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/subscription-status-ios.tsx @@ -0,0 +1,35 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function SubscriptionStatusIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + subscriptionStatusIOS +

    +

    Get detailed subscription status using StoreKit 2 (iOS 15+).

    + +

    Signature

    + + {{ + swift: ( + {`func subscriptionStatusIOS(sku: String) async throws -> [SubscriptionStatusIOS]`} + ), + }} + +
    + ); +} + +export default SubscriptionStatusIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/sync-ios.tsx b/packages/docs/src/pages/docs/apis/ios/sync-ios.tsx new file mode 100644 index 000000000..ff9a07702 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/sync-ios.tsx @@ -0,0 +1,34 @@ +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function SyncIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS syncIOS +

    +

    Force a StoreKit sync for transactions (iOS 15+).

    + +

    Signature

    + + {{ + swift: ( + {`func syncIOS() async throws -> Bool`} + ), + }} + +
    + ); +} + +export default SyncIOS; diff --git a/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx b/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx new file mode 100644 index 000000000..9072429f4 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/ios/validate-receipt-ios.tsx @@ -0,0 +1,48 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function ValidateReceiptIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + validateReceiptIOS +

    +

    Deprecated. Use verifyPurchase instead.

    + +
    +

    + Deprecated. Use the modern cross-platform validation + API. Use{' '} + + verifyPurchase + {' '} + instead. +

    +
    + +

    Signature

    + + {{ + swift: ( + {`@available(*, deprecated, message: "Use verifyPurchase()") +func validateReceiptIOS(options: ReceiptValidationProps) async throws -> ReceiptValidationResultIOS`} + ), + }} + +
    + ); +} + +export default ValidateReceiptIOS; diff --git a/packages/docs/src/pages/docs/apis/products.tsx b/packages/docs/src/pages/docs/apis/products.tsx deleted file mode 100644 index 8cd0d90bb..000000000 --- a/packages/docs/src/pages/docs/apis/products.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import { Link } from 'react-router-dom'; -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function ProductsAPIs() { - useScrollToHash(); - - return ( -
    - -

    Product APIs

    -

    - Retrieve product information and user's available purchases from the - store. -

    - - - - - -
    - - fetchProducts - -

    Retrieve products or subscriptions from the store by SKU.

    - -

    Signature

    - - {{ - typescript: ( - {`fetchProducts(params: ProductRequest): Promise - -interface ProductRequest { - skus: string[]; - type?: 'inapp' | 'subs' | 'all'; // Defaults to 'inapp' -}`} - ), - swift: ( - {`func fetchProducts(_ request: ProductRequest) async throws -> [Product]`} - ), - kotlin: ( - {`suspend fun fetchProducts(request: ProductRequest): List`} - ), - kmp: ( - {`suspend fun fetchProducts(request: ProductRequest): List`} - ), - dart: ( - {`Future> fetchProducts(ProductRequest request);`} - ), - gdscript: ( - {`func fetch_products(request: ProductRequest) -> Array[Product]`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { fetchProducts } from 'expo-iap'; - -// Fetch one-time products -const products = await fetchProducts({ - skus: ['com.app.coins_100', 'com.app.premium'], - type: 'inapp', -}); - -// Fetch subscriptions -const subscriptions = await fetchProducts({ - skus: ['com.app.monthly', 'com.app.yearly'], - type: 'subs', -}); - -// Display to user -products.forEach(product => { - console.log(\`\${product.title}: \${product.localizedPrice}\`); -});`} - ), - swift: ( - {`let products = try await OpenIapModule.shared.fetchProducts( - ProductRequest(skus: ["com.app.premium"], type: .inapp) -)`} - ), - kotlin: ( - {`val products = openIapStore.fetchProducts( - ProductRequest(skus = listOf("com.app.premium"), type = ProductQueryType.InApp) -)`} - ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -val products = kmpIAP.fetchProducts( - ProductRequest(skus = listOf("com.app.premium"), type = ProductQueryType.InApp) -)`} - ), - dart: ( - {`final products = await FlutterInappPurchase.instance.fetchProducts( - skus: ['com.app.premium'], -);`} - ), - gdscript: ( - {`# Fetch one-time products -var request = ProductRequest.new() -request.skus = ["com.app.coins_100", "com.app.premium"] -request.type = ProductQueryType.IN_APP -var products = await iap.fetch_products(request) - -# Fetch subscriptions -var subs_request = ProductRequest.new() -subs_request.skus = ["com.app.monthly", "com.app.yearly"] -subs_request.type = ProductQueryType.SUBS -var subscriptions = await iap.fetch_products(subs_request) - -# Display to user -for product in products: - print("%s: %s" % [product.title, product.display_price])`} - ), - }} - - -

    - See: Product,{' '} - SubscriptionProduct -

    -
    - -
    - - getAvailablePurchases - -

    - Get all available (unfinished) purchases for the current user. Use - this to restore purchases or check for pending transactions. -

    - -

    Signature

    - - {{ - typescript: ( - {`getAvailablePurchases(options?: PurchaseOptions): Promise - -interface PurchaseOptions { - alsoPublishToEventListenerIOS?: boolean; // iOS only - onlyIncludeActiveItemsIOS?: boolean; // iOS only -}`} - ), - swift: ( - {`func getAvailablePurchases(options: PurchaseOptions? = nil) async throws -> [Purchase]`} - ), - kotlin: ( - {`suspend fun getAvailablePurchases(): List`} - ), - kmp: ( - {`suspend fun getAvailablePurchases(): List`} - ), - dart: ( - {`Future> getAvailablePurchases({PurchaseOptions? options});`} - ), - gdscript: ( - {`func get_available_purchases(options: PurchaseOptions = null) -> Array[Purchase]`} - ), - }} - - -

    What it returns

    -
      -
    • - Consumables: Products not yet consumed -
    • -
    • - Non-consumables: Products not yet finished -
    • -
    • - Subscriptions: Currently active subscriptions -
    • -
    - -

    Example

    - - {{ - typescript: ( - {`import { getAvailablePurchases, finishTransaction } from 'expo-iap'; - -// Check for pending purchases on app launch -const purchases = await getAvailablePurchases(); - -for (const purchase of purchases) { - // Verify and finish each pending purchase - const verified = await verifyOnServer(purchase); - if (verified) { - await finishTransaction(purchase, false); - } -}`} - ), - swift: ( - {`let purchases = try await OpenIapModule.shared.getAvailablePurchases()`} - ), - kotlin: ( - {`val purchases = openIapStore.getAvailablePurchases()`} - ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -val purchases = kmpIAP.getAvailablePurchases()`} - ), - dart: ( - {`final purchases = await FlutterInappPurchase.instance.getAvailablePurchases();`} - ), - gdscript: ( - {`# Check for pending purchases on app launch -var purchases = await iap.get_available_purchases() - -for purchase in purchases: - # Verify and finish each pending purchase - var verified = await verify_on_server(purchase) - if verified: - await iap.finish_transaction(purchase, false)`} - ), - }} - - -
    -

    - Android limitation: For subscriptions with multiple - base plans, the currentPlanId field may be inaccurate. - See{' '} - - basePlanId limitation - - . -

    -
    - -

    - See: Purchase -

    -
    -
    - ); -} - -export default ProductsAPIs; diff --git a/packages/docs/src/pages/docs/apis/purchase.tsx b/packages/docs/src/pages/docs/apis/purchase.tsx deleted file mode 100644 index 9c484d51e..000000000 --- a/packages/docs/src/pages/docs/apis/purchase.tsx +++ /dev/null @@ -1,490 +0,0 @@ -import { Link } from 'react-router-dom'; -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function PurchaseAPIs() { - useScrollToHash(); - - return ( -
    - -

    Purchase APIs

    -

    - APIs for requesting purchases, completing transactions, and restoring - previous purchases. -

    - - - - - -
    - - Terminology - - - - Request APIs - -
    -

    - ⚠️ Important: APIs starting with{' '} - request are event-based operations, not promise-based. -

    -

    - While these APIs return values for various purposes, you should{' '} - - not rely on their return values for actual purchase results - - . Instead, listen for events through{' '} - purchaseUpdatedListener or{' '} - purchaseErrorListener. -

    -

    - This is because Apple's purchase system is fundamentally - event-based, not promise-based. For more details, see this{' '} - - issue comment - - . -

    -

    - The request prefix indicates that these are event - requests - use the appropriate listeners to handle the actual - results. -

    -
    -
    - -
    - - requestPurchase - -

    - Initiate a purchase flow. The result is delivered through{' '} - purchaseUpdatedListener, not the return value. -

    - -

    Signature

    - - {{ - typescript: ( - {`requestPurchase(props: RequestPurchaseProps): Promise - -type RequestPurchaseProps = - | { request: RequestPurchasePropsByPlatforms; type: 'inapp' } - | { request: RequestSubscriptionPropsByPlatforms; type: 'subs' }`} - ), - swift: ( - {`func requestPurchase(_ props: RequestPurchaseProps) async throws -> Purchase?`} - ), - kotlin: ( - {`suspend fun requestPurchase(props: RequestPurchaseProps): List`} - ), - kmp: ( - {`suspend fun requestPurchase(props: RequestPurchaseProps): List`} - ), - dart: ( - {`Future requestPurchase(RequestPurchaseProps props);`} - ), - gdscript: ( - {`func request_purchase(props: RequestPurchaseProps) -> Purchase`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { requestPurchase } from 'expo-iap'; - -// Purchase a one-time product -await requestPurchase({ - request: { - apple: { sku: 'com.app.premium' }, - google: { skus: ['com.app.premium'] }, - }, - type: 'inapp', -}); - -// Purchase a subscription -await requestPurchase({ - request: { - apple: { sku: 'com.app.monthly' }, - google: { - skus: ['com.app.monthly'], - subscriptionOffers: [{ - sku: 'com.app.monthly', - offerToken: 'offer-token-from-product', - }], - }, - }, - type: 'subs', -});`} - ), - swift: ( - {`try await OpenIapModule.shared.requestPurchase( - RequestPurchaseProps( - request: RequestPurchasePropsByPlatforms( - apple: RequestPurchaseIosProps(sku: "com.app.premium") - ), - type: .inapp - ) -)`} - ), - kotlin: ( - {`openIapStore.requestPurchase( - RequestPurchaseProps( - request = RequestPurchasePropsByPlatforms( - google = RequestPurchaseAndroidProps(skus = listOf("com.app.premium")) - ), - type = ProductQueryType.InApp - ) -)`} - ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -kmpIAP.requestPurchase( - RequestPurchaseProps( - request = RequestPurchasePropsByPlatforms( - google = RequestPurchaseAndroidProps(skus = listOf("com.app.premium")) - ), - type = ProductQueryType.InApp - ) -)`} - ), - dart: ( - {`await FlutterInappPurchase.instance.requestPurchase('com.app.premium');`} - ), - gdscript: ( - {`# Purchase a one-time product -var props = RequestPurchaseProps.new() -props.request = RequestPurchasePropsByPlatforms.new() -props.request.apple = RequestPurchaseIosProps.new() -props.request.apple.sku = "com.app.premium" -props.request.google = RequestPurchaseAndroidProps.new() -props.request.google.skus = ["com.app.premium"] -props.type = ProductQueryType.IN_APP - -await iap.request_purchase(props)`} - ), - }} - - -

    - See:{' '} - - RequestPurchaseProps - -

    -
    - -
    - - finishTransaction - -

    - Complete a purchase transaction. Must be called after - verifying the purchase to remove it from the queue. -

    - -

    Signature

    - - {{ - typescript: ( - {`finishTransaction(purchase: Purchase, isConsumable?: boolean): Promise`} - ), - swift: ( - {`func finishTransaction(_ purchase: Purchase) async throws`} - ), - kotlin: ( - {`suspend fun finishTransaction(purchase: Purchase, isConsumable: Boolean = false)`} - ), - kmp: ( - {`suspend fun finishTransaction(purchase: Purchase, isConsumable: Boolean = false)`} - ), - dart: ( - {`Future finishTransaction(Purchase purchase, {bool isConsumable = false});`} - ), - gdscript: ( - {`func finish_transaction(purchase: Purchase, is_consumable: bool = false) -> void`} - ), - }} - - -

    isConsumable Parameter

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    TypeisConsumableBehavior
    Consumable - true - Product can be purchased again (coins, gems)
    Non-consumable - false - One-time purchase (premium unlock)
    Subscription - false - Managed by the store
    - -

    Example

    - - {{ - typescript: ( - {`import { finishTransaction, purchaseUpdatedListener } from 'expo-iap'; - -purchaseUpdatedListener(async (purchase) => { - // 1. Verify on your server - const verified = await verifyOnServer(purchase); - if (!verified) return; - - // 2. Grant entitlement to user - await grantProduct(purchase.productId); - - // 3. Finish the transaction - const isConsumable = purchase.productId.includes('coins'); - await finishTransaction(purchase, isConsumable); -});`} - ), - swift: ( - {`try await OpenIapModule.shared.finishTransaction(purchase, isConsumable: false)`} - ), - kotlin: ( - {`openIapStore.finishTransaction(purchase, isConsumable = false)`} - ), - kmp: ( - {`kmpIAP.finishTransaction(purchase, isConsumable = false)`} - ), - dart: ( - {`await FlutterInappPurchase.instance.finishTransaction(purchase);`} - ), - gdscript: ( - {`# Handle purchase update -func _on_purchase_updated(purchase: Purchase): - # 1. Verify on your server - var verified = await verify_on_server(purchase) - if not verified: - return - - # 2. Grant entitlement to user - await grant_product(purchase.product_id) - - # 3. Finish the transaction - var is_consumable = "coins" in purchase.product_id - await iap.finish_transaction(purchase, is_consumable)`} - ), - }} - - -
    -

    - Critical: Android purchases must be acknowledged - within 3 days or they will be automatically refunded. iOS - transactions will replay on every app launch if not finished. -

    -
    -
    - -
    - - restorePurchases - -

    - Restore completed transactions. Use this to implement a "Restore - Purchases" button for users who reinstall the app. -

    - -

    Signature

    - - {{ - typescript: ( - {`restorePurchases(): Promise`} - ), - swift: ( - {`func restorePurchases() async throws`} - ), - kotlin: ( - {`suspend fun restorePurchases()`} - ), - kmp: ( - {`suspend fun restorePurchases()`} - ), - dart: ( - {`Future restorePurchases();`} - ), - gdscript: ( - {`func restore_purchases() -> void`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { restorePurchases, getAvailablePurchases } from 'expo-iap'; - -const handleRestore = async () => { - await restorePurchases(); - const purchases = await getAvailablePurchases(); - - for (const purchase of purchases) { - // Re-grant entitlements - await grantProduct(purchase.productId); - } -};`} - ), - swift: ( - {`try await OpenIapModule.shared.restorePurchases()`} - ), - kotlin: ( - {`openIapStore.restorePurchases()`} - ), - kmp: ( - {`kmpIAP.restorePurchases()`} - ), - dart: ( - {`await FlutterInappPurchase.instance.restorePurchases();`} - ), - gdscript: ( - {`func _on_restore_pressed(): - await iap.restore_purchases() - var purchases = await iap.get_available_purchases() - - for purchase in purchases: - # Re-grant entitlements - await grant_product(purchase.product_id)`} - ), - }} - -
    - -
    - - getStorefront - -

    Get the storefront country code for the active user.

    - -

    Signature

    - - {{ - typescript: ( - {`getStorefront(): Promise`} - ), - swift: ( - {`func getStorefront() async throws -> String`} - ), - kotlin: ( - {`suspend fun getStorefront(): String`} - ), - kmp: ( - {`suspend fun getStorefront(): String`} - ), - dart: ( - {`Future getStorefront();`} - ), - gdscript: ( - {`func get_storefront() -> String`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { getStorefront } from 'expo-iap'; - -const countryCode = await getStorefront(); -console.log(countryCode); // "US", "JP", "GB", etc.`} - ), - swift: ( - {`let countryCode = try await OpenIapModule.shared.getStorefront()`} - ), - kotlin: ( - {`val countryCode = openIapStore.getStorefront()`} - ), - kmp: ( - {`val countryCode = kmpIAP.getStorefront()`} - ), - dart: ( - {`final countryCode = await FlutterInappPurchase.instance.getStorefront();`} - ), - gdscript: ( - {`var country_code = await iap.get_storefront() -print(country_code) # "US", "JP", "GB", etc.`} - ), - }} - - -

    - Returns the ISO 3166-1 alpha-2 country code. Returns an empty string - when the storefront cannot be determined. -

    -
    -
    - ); -} - -export default PurchaseAPIs; diff --git a/packages/docs/src/pages/docs/apis/request-purchase.tsx b/packages/docs/src/pages/docs/apis/request-purchase.tsx new file mode 100644 index 000000000..ade42a18d --- /dev/null +++ b/packages/docs/src/pages/docs/apis/request-purchase.tsx @@ -0,0 +1,267 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function RequestPurchase() { + useScrollToHash(); + + return ( +
    + +

    requestPurchase

    +

    + Initiate a purchase flow. The result is delivered through + purchaseUpdatedListener, not the return value. +

    + +
    +

    + ⚠️ Important: APIs starting with request{' '} + are event-based operations, not promise-based. +

    +

    + While these APIs return values for various purposes, you should{' '} + + not rely on their return values for actual purchase results + + . Instead, listen for events through{' '} + + purchaseUpdatedListener + {' '} + or{' '} + + purchaseErrorListener + + . +

    +

    + This is because Apple's purchase system is fundamentally event-based, + not promise-based. For more details, see{' '} + + this issue comment + + . +

    +

    + The request prefix indicates that these are event + requests — use the appropriate listeners to handle the actual results. +

    +
    + +

    Signature

    + + {{ + typescript: ( + {`requestPurchase(props: RequestPurchaseProps): Promise + +type RequestPurchaseProps = + | { request: RequestPurchasePropsByPlatforms; type: 'in-app' } + | { request: RequestSubscriptionPropsByPlatforms; type: 'subs' }`} + ), + swift: ( + {`func requestPurchase(_ params: RequestPurchaseProps) async throws -> RequestPurchaseResult?`} + ), + kotlin: ( + {`suspend fun requestPurchase(props: RequestPurchaseProps): Purchase?`} + ), + kmp: ( + {`suspend fun requestPurchase(props: RequestPurchaseProps): Purchase?`} + ), + dart: ( + {`Future requestPurchase(RequestPurchaseProps props);`} + ), + gdscript: ( + {`func request_purchase(props: RequestPurchaseProps) -> Purchase`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { requestPurchase } from 'expo-iap'; +// Same API in react-native-iap: +// import { requestPurchase } from 'react-native-iap'; + +// One-time product +await requestPurchase({ + request: { + apple: { sku: 'com.app.premium' }, + google: { skus: ['com.app.premium'] }, + }, + type: 'in-app', +}); + +// Subscription +await requestPurchase({ + request: { + apple: { sku: 'com.app.monthly' }, + google: { + skus: ['com.app.monthly'], + subscriptionOffers: [{ sku: 'com.app.monthly', offerToken: 'offer-token' }], + }, + }, + type: 'subs', +}); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP wires the purchase listeners for you and exposes the same +// requestPurchase function — handle the result inside onPurchaseSuccess. +import { useIAP } from 'expo-iap'; + +function BuyButton({ sku }: { sku: string }) { + const { requestPurchase } = useIAP({ + onPurchaseSuccess: async (purchase) => { + // verify + finishTransaction here + }, + onPurchaseError: (error) => { + console.warn('Purchase failed', error); + }, + }); + + return ( +
    + ); +} + +export default RequestPurchase; diff --git a/packages/docs/src/pages/docs/apis/restore-purchases.tsx b/packages/docs/src/pages/docs/apis/restore-purchases.tsx new file mode 100644 index 000000000..92c7d9448 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/restore-purchases.tsx @@ -0,0 +1,141 @@ +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function RestorePurchases() { + useScrollToHash(); + + return ( +
    + +

    restorePurchases

    +

    + Restore completed transactions. Use this to implement a "Restore + Purchases" button for users who reinstall the app. +

    + +

    Signature

    + + {{ + typescript: ( + {`restorePurchases(): Promise`} + ), + swift: ( + {`func restorePurchases() async throws`} + ), + kotlin: ( + {`suspend fun restorePurchases()`} + ), + kmp: ( + {`suspend fun restorePurchases()`} + ), + dart: ( + {`Future restorePurchases();`} + ), + gdscript: ( + {`func restore_purchases() -> void`} + ), + }} + + +

    Example

    + + {{ + typescript: ( + {`// expo-iap +import { + restorePurchases, + getAvailablePurchases, + verifyPurchase, + finishTransaction, +} from 'expo-iap'; +// Same API in react-native-iap: +// import { +// restorePurchases, +// getAvailablePurchases, +// verifyPurchase, +// finishTransaction, +// } from 'react-native-iap'; + +const handleRestore = async () => { + await restorePurchases(); + const purchases = await getAvailablePurchases(); + + for (const purchase of purchases) { + // Always verify before granting — restored purchases can include + // refunded or revoked transactions that must not re-grant entitlement. + const result = await verifyPurchase({ + purchase, + serverUrl: 'https://your-server.com/api/verify', + }); + if (!result.isValid) continue; + + await grantProduct(purchase.productId); + await finishTransaction({ purchase, isConsumable: false }); + } +}; + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP's restorePurchases() and getAvailablePurchases() both return +// Promise and update the reactive availablePurchases array — react +// to it inside an effect. +import { useIAP } from 'expo-iap'; + +function RestoreButton() { + const { + availablePurchases, + restorePurchases, + getAvailablePurchases, + finishTransaction, + } = useIAP(); + + const handleRestore = async () => { + await restorePurchases(); + await getAvailablePurchases(); + }; + + useEffect(() => { + (async () => { + for (const purchase of availablePurchases) { + const result = await verifyPurchase({ + purchase, + serverUrl: 'https://your-server.com/api/verify', + }); + if (!result.isValid) continue; + await grantProduct(purchase.productId); + await finishTransaction({ purchase, isConsumable: false }); + } + })(); + }, [availablePurchases, finishTransaction]); + + return
    + ); +} + +export default RestorePurchases; diff --git a/packages/docs/src/pages/docs/apis/subscription.tsx b/packages/docs/src/pages/docs/apis/subscription.tsx deleted file mode 100644 index 49efc638d..000000000 --- a/packages/docs/src/pages/docs/apis/subscription.tsx +++ /dev/null @@ -1,372 +0,0 @@ -import { Link } from 'react-router-dom'; -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function SubscriptionAPIs() { - useScrollToHash(); - - return ( -
    - -

    Subscription APIs

    -

    - APIs for managing auto-renewable subscriptions, checking status, and - opening subscription management. -

    - - - - - -
    - - getActiveSubscriptions - -

    - Get all active subscriptions with detailed renewal status information. -

    - -

    Signature

    - - {{ - typescript: ( - {`getActiveSubscriptions(subscriptionIds?: string[]): Promise`} - ), - swift: ( - {`func getActiveSubscriptions(subscriptionIds: [String]? = nil) async throws -> [ActiveSubscription]`} - ), - kotlin: ( - {`suspend fun getActiveSubscriptions(subscriptionIds: List? = null): List`} - ), - kmp: ( - {`suspend fun getActiveSubscriptions(subscriptionIds: List? = null): List`} - ), - dart: ( - {`Future> getActiveSubscriptions({List? subscriptionIds});`} - ), - gdscript: ( - {`func get_active_subscriptions(subscription_ids: Array[String] = []) -> Array[ActiveSubscription]`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { getActiveSubscriptions } from 'expo-iap'; - -// Get all active subscriptions -const subscriptions = await getActiveSubscriptions(); - -// Or filter by specific IDs -const premiumSubs = await getActiveSubscriptions(['premium_monthly', 'premium_yearly']); - -for (const sub of subscriptions) { - console.log(\`Product: \${sub.productId}\`); - console.log(\`Expires: \${sub.expirationDate}\`); - - // iOS: Check renewal status - if (sub.renewalInfoIOS?.willAutoRenew === false) { - console.log('Subscription cancelled, will not renew'); - } - - // iOS: Check for pending upgrade - if (sub.renewalInfoIOS?.pendingUpgradeProductId) { - console.log(\`Upgrading to \${sub.renewalInfoIOS.pendingUpgradeProductId}\`); - } -}`} - ), - swift: ( - {`let subscriptions = try await OpenIapModule.shared.getActiveSubscriptions() - -for sub in subscriptions { - if sub.renewalInfoIOS?.willAutoRenew == false { - print("Subscription cancelled") - } -}`} - ), - kotlin: ( - {`val subscriptions = openIapStore.getActiveSubscriptions() - -for (sub in subscriptions) { - if (sub.autoRenewingAndroid == false) { - println("Subscription cancelled") - } -}`} - ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -val subscriptions = kmpIAP.getActiveSubscriptions() - -for (sub in subscriptions) { - if (sub.autoRenewingAndroid == false) { - println("Subscription cancelled") - } -}`} - ), - dart: ( - {`final subscriptions = await FlutterInappPurchase.instance.getActiveSubscriptions();`} - ), - gdscript: ( - {`# Get all active subscriptions -var subscriptions = await iap.get_active_subscriptions() - -# Or filter by specific IDs -var premium_subs = await iap.get_active_subscriptions(["premium_monthly", "premium_yearly"]) - -for sub in subscriptions: - print("Product: %s" % sub.product_id) - print("Expires: %s" % sub.expiration_date_ios) - - # Check renewal status (Android) - if not sub.auto_renewing_android: - print("Subscription cancelled, will not renew")`} - ), - }} - - -
    -

    - iOS Renewal Info: Each subscription includes{' '} - renewalInfoIOS with: -

    -
      -
    • - willAutoRenew - Whether subscription will auto-renew -
    • -
    • - pendingUpgradeProductId - Product ID of pending - upgrade -
    • -
    • - renewalDate - Next renewal date -
    • -
    • - expirationReason - Why subscription expired -
    • -
    -
    - -

    - See:{' '} - ActiveSubscription -

    -
    - -
    - - hasActiveSubscriptions - -

    Quick check if the user has any active subscriptions.

    - -

    Signature

    - - {{ - typescript: ( - {`hasActiveSubscriptions(subscriptionIds?: string[]): Promise`} - ), - swift: ( - {`func hasActiveSubscriptions(subscriptionIds: [String]? = nil) async throws -> Bool`} - ), - kotlin: ( - {`suspend fun hasActiveSubscriptions(subscriptionIds: List? = null): Boolean`} - ), - kmp: ( - {`suspend fun hasActiveSubscriptions(subscriptionIds: List? = null): Boolean`} - ), - dart: ( - {`Future hasActiveSubscriptions({List? subscriptionIds});`} - ), - gdscript: ( - {`func has_active_subscriptions(subscription_ids: Array[String] = []) -> bool`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { hasActiveSubscriptions } from 'expo-iap'; - -// Check any subscription -const isPremium = await hasActiveSubscriptions(); - -// Check specific subscriptions -const hasProPlan = await hasActiveSubscriptions(['pro_monthly', 'pro_yearly']); - -if (isPremium) { - // Show premium features -}`} - ), - swift: ( - {`let isPremium = try await OpenIapModule.shared.hasActiveSubscriptions()`} - ), - kotlin: ( - {`val isPremium = openIapStore.hasActiveSubscriptions()`} - ), - kmp: ( - {`val isPremium = kmpIAP.hasActiveSubscriptions()`} - ), - dart: ( - {`final isPremium = await FlutterInappPurchase.instance.hasActiveSubscriptions();`} - ), - gdscript: ( - {`# Check any subscription -var is_premium = await iap.has_active_subscriptions() - -# Check specific subscriptions -var has_pro_plan = await iap.has_active_subscriptions(["pro_monthly", "pro_yearly"]) - -if is_premium: - # Show premium features - pass`} - ), - }} - -
    - -
    - - deepLinkToSubscriptions - -

    - Open the native subscription management interface where users can view - and manage their subscriptions. -

    - -

    Signature

    - - {{ - typescript: ( - {`deepLinkToSubscriptions(options: DeepLinkOptions): Promise - -interface DeepLinkOptions { - skuAndroid?: string; // Required on Android - packageNameAndroid?: string; // Required on Android -}`} - ), - swift: ( - {`func deepLinkToSubscriptions() async throws`} - ), - kotlin: ( - {`suspend fun deepLinkToSubscriptions(options: DeepLinkOptions)`} - ), - kmp: ( - {`suspend fun deepLinkToSubscriptions(options: DeepLinkOptions)`} - ), - dart: ( - {`Future deepLinkToSubscriptions({String? skuAndroid, String? packageNameAndroid});`} - ), - gdscript: ( - {`func deep_link_to_subscriptions(options: DeepLinkOptions) -> void`} - ), - }} - - -

    Example

    - - {{ - typescript: ( - {`import { deepLinkToSubscriptions } from 'expo-iap'; -import { Platform } from 'react-native'; - -const openSubscriptionManagement = async () => { - await deepLinkToSubscriptions({ - // Android requires these - skuAndroid: 'com.app.premium', - packageNameAndroid: 'com.yourcompany.app', - }); -};`} - ), - swift: ( - {`// Opens Settings app subscription management -try await OpenIapModule.shared.deepLinkToSubscriptions()`} - ), - kotlin: ( - {`openIapStore.deepLinkToSubscriptions( - DeepLinkOptions( - skuAndroid = "com.app.premium", - packageNameAndroid = "com.yourcompany.app" - ) -)`} - ), - kmp: ( - {`kmpIAP.deepLinkToSubscriptions( - DeepLinkOptions( - skuAndroid = "com.app.premium", - packageNameAndroid = "com.yourcompany.app" - ) -)`} - ), - dart: ( - {`await FlutterInappPurchase.instance.deepLinkToSubscriptions( - skuAndroid: 'com.app.premium', - packageNameAndroid: 'com.yourcompany.app', -);`} - ), - gdscript: ( - {`# Open subscription management (Android) -var options = DeepLinkOptions.new() -options.sku_android = "com.app.premium" -options.package_name_android = "com.yourcompany.app" -await iap.deep_link_to_subscriptions(options)`} - ), - }} - - -

    - Platform behavior: -

    -
      -
    • - iOS: Opens the Settings app subscription - management. Also see{' '} - - showManageSubscriptionsIOS - {' '} - for an in-app UI. -
    • -
    • - Android: Opens Google Play subscription management - for the specified SKU. -
    • -
    -
    -
    - ); -} - -export default SubscriptionAPIs; diff --git a/packages/docs/src/pages/docs/apis/validate-receipt.tsx b/packages/docs/src/pages/docs/apis/validate-receipt.tsx new file mode 100644 index 000000000..0bb789766 --- /dev/null +++ b/packages/docs/src/pages/docs/apis/validate-receipt.tsx @@ -0,0 +1,14 @@ +import { Navigate } from 'react-router-dom'; + +// Cross-platform `validateReceipt` is deprecated in the schema in favour +// of `verifyPurchase`. Bookmarks that hit /docs/apis/validate-receipt +// bounce to the canonical Validation feature page, so old links keep +// working without us maintaining a parallel reference. Use +// so the redirect happens declaratively during +// render — no flash of intermediate content, no extra effect-driven +// re-render. +function ValidateReceipt() { + return ; +} + +export default ValidateReceipt; diff --git a/packages/docs/src/pages/docs/errors.tsx b/packages/docs/src/pages/docs/errors.tsx index 0ec395322..540b83578 100644 --- a/packages/docs/src/pages/docs/errors.tsx +++ b/packages/docs/src/pages/docs/errors.tsx @@ -19,11 +19,10 @@ function Errors() {

    Error Codes

    -

    Error Structure

    +

    Error Structure

    All purchase errors follow a consistent structure for easy handling. - See PurchaseError type{' '} - for details. + The PurchaseError shape is defined below.

    {{ diff --git a/packages/docs/src/pages/docs/events.tsx b/packages/docs/src/pages/docs/events.tsx index 8c796a041..6d747c866 100644 --- a/packages/docs/src/pages/docs/events.tsx +++ b/packages/docs/src/pages/docs/events.tsx @@ -17,9 +17,18 @@ function Events() { keywords="IAP events, purchaseUpdatedListener, purchaseErrorListener, purchase listener, transaction events, async purchase handling" />

    Events

    +

    + Complete listener reference for OpenIAP. Every event listener is listed + below with a one-line description and a link to its full signature. The + IAP library uses an event-driven architecture to handle purchase flows + asynchronously — set up listeners before initiating any purchase to + properly handle the results. +

    -

    Event System Overview

    + + Event System Overview +

    The IAP library uses an event-driven architecture to handle purchase flows asynchronously. You must set up event listeners before @@ -33,6 +42,7 @@ function Events() { {`enum IapEvent { PurchaseUpdated = 'purchaseUpdated', PurchaseError = 'purchaseError', + SubscriptionBillingIssue = 'subscriptionBillingIssue', PromotedProductIOS = 'promotedProductIOS', UserChoiceBillingAndroid = 'userChoiceBillingAndroid', DeveloperProvidedBillingAndroid = 'developerProvidedBillingAndroid', // 8.3.0+ @@ -42,6 +52,7 @@ function Events() { {`enum IapEvent { case purchaseUpdated case purchaseError + case subscriptionBillingIssue case promotedProductIOS }`} ), @@ -49,6 +60,7 @@ function Events() { {`enum class IapEvent { PurchaseUpdated, PurchaseError, + SubscriptionBillingIssue, UserChoiceBillingAndroid, DeveloperProvidedBillingAndroid // 8.3.0+ }`} @@ -57,6 +69,7 @@ function Events() { {`enum class IapEvent { PurchaseUpdated, PurchaseError, + SubscriptionBillingIssue, UserChoiceBillingAndroid, DeveloperProvidedBillingAndroid // 8.3.0+ }`} @@ -65,6 +78,7 @@ function Events() { {`enum IapEvent { purchaseUpdated, purchaseError, + subscriptionBillingIssue, promotedProductIOS, userChoiceBillingAndroid, developerProvidedBillingAndroid, // 8.3.0+ @@ -74,1182 +88,125 @@ function Events() { {`enum IapEvent { PURCHASE_UPDATED = 0, PURCHASE_ERROR = 1, - PROMOTED_PRODUCT_IOS = 2, - USER_CHOICE_BILLING_ANDROID = 3, - DEVELOPER_PROVIDED_BILLING_ANDROID = 4, # 8.3.0+ -}`} - ), - }} - -

    - -
    - - Purchase Updated Event - -

    - Fired when a purchase is successful or when a pending purchase is - completed. -

    - -

    Listener Setup

    - - {{ - typescript: ( - {`purchaseUpdatedListener( - listener: (purchase: Purchase) => void -): Subscription`} - ), - swift: ( - {`// AsyncSequence approach -var purchaseUpdates: AsyncStream - -// Combine approach -var purchaseUpdatedPublisher: AnyPublisher`} - ), - kotlin: ( - {`// Flow approach -val purchaseUpdates: Flow`} - ), - kmp: ( - {`// Flow approach -val purchaseUpdates: Flow`} - ), - dart: ( - {`Stream get purchaseUpdatedStream;`} - ), - gdscript: ( - {`signal purchase_updated(purchase: Purchase)`} - ), - }} - -

    Registers a listener for successful purchase events.

    - - - {{ - typescript: ( - {`import { purchaseUpdatedListener } from 'expo-iap'; - -const subscription = purchaseUpdatedListener(async (purchase) => { - console.log('Purchase updated:', purchase.productId); - - // Validate the receipt - const isValid = await validateReceipt(purchase); - - if (isValid) { - // Deliver content to user - await deliverProduct(purchase.productId); - - // Finish the transaction - await finishTransaction(purchase, { isConsumable: false }); - } -}); - -// Cleanup when done -subscription.remove();`} - ), - swift: ( - {`import OpenIap - -// Using async/await -Task { - for await purchase in OpenIapModule.shared.purchaseUpdates { - print("Purchase updated: \\(purchase.productId)") - - // Validate and deliver - if await validateReceipt(purchase) { - await deliverProduct(purchase.productId) - try await OpenIapModule.shared.finishTransaction(purchase) - } - } -} - -// Or using Combine -OpenIapModule.shared.purchaseUpdatedPublisher - .sink { purchase in - print("Purchase updated: \\(purchase.productId)") - } - .store(in: &cancellables)`} - ), - kotlin: ( - {`import dev.hyo.openiap.OpenIapStore - -// Using Flow -lifecycleScope.launch { - openIapStore.purchaseUpdates.collect { purchase -> - println("Purchase updated: \${purchase.productId}") - - // Validate and deliver - if (validateReceipt(purchase)) { - deliverProduct(purchase.productId) - openIapStore.finishTransaction(purchase, isConsumable = false) - } - } -} - -// Or with callback -openIapStore.setPurchaseUpdatedListener { purchase -> - println("Purchase updated: \${purchase.productId}") -}`} - ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -// Using Flow -lifecycleScope.launch { - kmpIAP.purchaseUpdates.collect { purchase -> - println("Purchase updated: \${purchase.productId}") - - // Validate and deliver - if (validateReceipt(purchase)) { - deliverProduct(purchase.productId) - kmpIAP.finishTransaction(purchase, isConsumable = false) - } - } -} - -// Or with callback -kmpIAP.setPurchaseUpdatedListener { purchase -> - println("Purchase updated: \${purchase.productId}") -}`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -final subscription = FlutterInappPurchase.purchaseUpdated.listen((purchase) async { - print('Purchase updated: \${purchase?.productId}'); - - // Validate the receipt - final isValid = await validateReceipt(purchase); - - if (isValid) { - // Deliver content to user - await deliverProduct(purchase!.productId); - - // Finish the transaction - await FlutterInappPurchase.instance.finishTransaction(purchase); - } -}); - -// Cleanup when done -subscription.cancel();`} - ), - gdscript: ( - {`# Connect to the signal -iap.purchase_updated.connect(_on_purchase_updated) - -func _on_purchase_updated(purchase: Purchase): - print("Purchase updated: %s" % purchase.product_id) - - # Validate the receipt - var is_valid = await validate_receipt(purchase) - - if is_valid: - # Deliver content to user - await deliver_product(purchase.product_id) - - # Finish the transaction - await iap.finish_transaction(purchase, false) - -# Cleanup when done -func _exit_tree(): - iap.purchase_updated.disconnect(_on_purchase_updated)`} - ), - }} - - -

    Event Payload

    -

    - The purchase event delivers a{' '} - Purchase object containing - transaction details. -

    - -

    Purchase Update Flow

    -
      -
    1. - Receive Purchase object via - listener -
    2. -
    3. Validate receipt with backend service
    4. -
    5. Deliver purchased content to user
    6. -
    7. - Finish transaction with{' '} - - finishTransaction - {' '} - (handles acknowledgment on both platforms) -
    8. -
    9. Update application state
    10. -
    -
    - -
    - - Purchase Error Event - -

    Fired when a purchase fails or is cancelled by the user.

    - -

    Listener Setup

    - - {{ - typescript: ( - {`purchaseErrorListener( - listener: (error: PurchaseError) => void -): Subscription`} - ), - swift: ( - {`// AsyncSequence approach -var purchaseErrors: AsyncStream - -// Combine approach -var purchaseErrorPublisher: AnyPublisher`} - ), - kotlin: ( - {`// Flow approach -val purchaseErrors: Flow`} - ), - kmp: ( - {`// Flow approach -val purchaseErrors: Flow`} - ), - dart: ( - {`Stream get purchaseErrorStream;`} - ), - }} - -

    Registers a listener for purchase error events.

    - - - {{ - typescript: ( - {`import { purchaseErrorListener, ErrorCode } from 'expo-iap'; - -const subscription = purchaseErrorListener((error) => { - console.log('Purchase error:', error.code, error.message); - - switch (error.code) { - case ErrorCode.UserCancelled: - // User cancelled - no action needed - break; - case ErrorCode.AlreadyOwned: - // Restore purchases instead - restorePurchases(); - break; - case ErrorCode.NetworkError: - // Show retry option - showRetryDialog(); - break; - default: - showErrorMessage(error.message); - } -}); - -// Cleanup when done -subscription.remove();`} - ), - swift: ( - {`import OpenIap - -// Using async/await -Task { - for await error in OpenIapModule.shared.purchaseErrors { - print("Purchase error: \\(error.code) - \\(error.message)") - - switch error.code { - case .userCancelled: - // User cancelled - no action needed - break - case .alreadyOwned: - // Restore purchases instead - try await OpenIapModule.shared.restorePurchases() - case .networkError: - showRetryDialog() - default: - showErrorMessage(error.message) - } - } -} - -// Or using Combine -OpenIapModule.shared.purchaseErrorPublisher - .sink { error in - print("Purchase error: \\(error.code)") - } - .store(in: &cancellables)`} - ), - kotlin: ( - {`import dev.hyo.openiap.OpenIapError - -// Using Flow -lifecycleScope.launch { - openIapStore.purchaseErrors.collect { error -> - println("Purchase error: \${error.code} - \${error.message}") - - when (error.code) { - OpenIapError.UserCancelled -> { - // User cancelled - no action needed - } - OpenIapError.AlreadyOwned -> { - // Restore purchases instead - openIapStore.restorePurchases() - } - OpenIapError.NetworkError -> { - showRetryDialog() - } - else -> { - showErrorMessage(error.message) - } - } - } -} - -// Or with callback -openIapStore.setPurchaseErrorListener { error -> - println("Purchase error: \${error.code}") + SUBSCRIPTION_BILLING_ISSUE = 2, + PROMOTED_PRODUCT_IOS = 3, + USER_CHOICE_BILLING_ANDROID = 4, + DEVELOPER_PROVIDED_BILLING_ANDROID = 5, # 8.3.0+ }`} ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -// Using Flow -lifecycleScope.launch { - kmpIAP.purchaseErrors.collect { error -> - println("Purchase error: \${error.code} - \${error.message}") - - when (error.code) { - OpenIapError.UserCancelled -> { - // User cancelled - no action needed - } - OpenIapError.AlreadyOwned -> { - // Restore purchases instead - kmpIAP.restorePurchases() - } - OpenIapError.NetworkError -> { - showRetryDialog() - } - else -> { - showErrorMessage(error.message) - } - } - } -} - -// Or with callback -kmpIAP.setPurchaseErrorListener { error -> - println("Purchase error: \${error.code}") -}`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -final subscription = FlutterInappPurchase.purchaseError.listen((error) { - print('Purchase error: \${error?.code} - \${error?.message}'); - - switch (error?.code) { - case 'E_USER_CANCELLED': - // User cancelled - no action needed - break; - case 'E_ALREADY_OWNED': - // Restore purchases instead - FlutterInappPurchase.instance.restorePurchases(); - break; - case 'E_NETWORK_ERROR': - showRetryDialog(); - break; - default: - showErrorMessage(error?.message ?? 'Unknown error'); - } -}); - -// Cleanup when done -subscription.cancel();`} - ), - }} - - -

    Error Payload

    -

    - The error event delivers a{' '} - PurchaseError object with error - details. See Error Codes for complete - reference. -

    - -

    Error Handling Strategy

    -

    - Handle errors based on their{' '} - error codes: -

    -
      -
    • - UserCancelled - No action required -
    • -
    • - ItemUnavailable - Check product availability -
    • -
    • - NetworkError - Retry with backoff -
    • -
    • - AlreadyOwned - Restore purchases -
    • -
    • - ReceiptFailed - Retry validation -
    • -
    -
    - -
    - - Subscription Billing Issue Event - -

    - Fired when an active subscription enters a state that needs user - attention because of a payment problem — card declined, expired - payment method, billing retry, or grace period. Unifies StoreKit 2{' '} - Message.billingIssue (iOS 18+) and Play Billing{' '} - Purchase.isSuspended (Play Billing Library 8.1+) under a - single cross-platform stream. Silent no-op on platforms that cannot - emit (tvOS, watchOS, visionOS, macOS, Meta Horizon). -

    - -

    Listener Setup

    - - {{ - typescript: ( - {`subscriptionBillingIssueListener( - listener: (purchase: Purchase) => void -): Subscription`} - ), - swift: ( - {`// Callback + Subscription handle (iOS 18+ only) -func subscriptionBillingIssueListener( - _ listener: @escaping @Sendable (Purchase) -> Void -) -> Subscription`} - ), - kotlin: ( - {`// Callback approach (Play Billing 8.1+) -fun addSubscriptionBillingIssueListener( - listener: OpenIapSubscriptionBillingIssueListener -)`} - ), - kmp: ( - {`// Flow approach -val subscriptionBillingIssueListener: Flow`} - ), - dart: ( - {`Stream get subscriptionBillingIssueListener;`} - ), - gdscript: ( - {`signal subscription_billing_issue(purchase: Purchase)`} - ), }} -

    - The emitted Purchase is a regular subscription payload — - use productId, purchaseToken, and platform - fields to prompt the user to update payment. Play deduplicates by{' '} - purchaseToken per session; iOS fires per Message - delivery. -

    - -

    - See{' '} - - Subscription Billing Issue feature guide - {' '} - for platform coverage, signal sources, and UX recommendations. -

    -
    - -
    - - User Choice Billing Event (Android) - -

    - Fired when a user selects alternative billing in the User Choice - Billing dialog on Android. -

    - -

    Listener Setup

    - - {{ - typescript: ( - {`userChoiceBillingListenerAndroid( - listener: (details: UserChoiceBillingDetails) => void -): Subscription`} - ), - swift: ( - {`// Android only - not available on iOS`} - ), - kotlin: ( - {`// Flow approach -val userChoiceBillingEvents: Flow`} - ), - kmp: ( - {`// Flow approach -val userChoiceBillingEvents: Flow`} - ), - dart: ( - {`Stream get userChoiceBillingStream; -// Android only`} - ), - }} - -

    - Registers a listener for User Choice Billing events. This listener is - only triggered when the user selects alternative billing instead of - Google Play billing. -

    - - - {{ - typescript: ( - {`import { userChoiceBillingListenerAndroid } from 'expo-iap'; - -const subscription = userChoiceBillingListenerAndroid(async (details) => { - console.log('User chose alternative billing'); - console.log('Products:', details.products); - console.log('Token:', details.externalTransactionToken); - - // Process payment with your backend - const paymentResult = await processPaymentWithBackend({ - products: details.products, - token: details.externalTransactionToken, - }); - - if (paymentResult.success) { - // Backend should report token to Google Play within 24 hours - grantUserAccess(details.products); - } -}); - -// Cleanup when done -subscription.remove();`} - ), - kotlin: ( - {`import dev.hyo.openiap.UserChoiceBillingDetails - -// Using Flow -lifecycleScope.launch { - openIapStore.userChoiceBillingEvents.collect { details -> - println("User chose alternative billing") - println("Products: \${details.products}") - println("Token: \${details.externalTransactionToken}") - - // Process payment with your backend - val paymentResult = processPaymentWithBackend( - products = details.products, - token = details.externalTransactionToken - ) - - if (paymentResult.success) { - // Backend should report token to Google Play within 24 hours - grantUserAccess(details.products) - } - } -} - -// Or with callback -openIapStore.setUserChoiceBillingListener { details -> - println("User chose alternative billing for: \${details.products}") -}`} - ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -// Using Flow -lifecycleScope.launch { - kmpIAP.userChoiceBillingEvents.collect { details -> - println("User chose alternative billing") - println("Products: \${details.products}") - println("Token: \${details.externalTransactionToken}") - - // Process payment with your backend - val paymentResult = processPaymentWithBackend( - products = details.products, - token = details.externalTransactionToken - ) - - if (paymentResult.success) { - // Backend should report token to Google Play within 24 hours - grantUserAccess(details.products) - } - } -} - -// Or with callback -kmpIAP.setUserChoiceBillingListener { details -> - println("User chose alternative billing for: \${details.products}") -}`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -// Android only - will not fire on iOS -final subscription = FlutterInappPurchase.userChoiceBillingAndroid.listen((details) async { - print('User chose alternative billing'); - print('Products: \${details?.products}'); - print('Token: \${details?.externalTransactionToken}'); - - // Process payment with your backend - final paymentResult = await processPaymentWithBackend( - products: details!.products, - token: details.externalTransactionToken, - ); - - if (paymentResult.success) { - // Backend should report token to Google Play within 24 hours - grantUserAccess(details.products); - } -}); - -// Cleanup when done -subscription.cancel();`} - ), - }} - - -

    Event Payload

    - - {{ - typescript: ( - {`interface UserChoiceBillingDetails { - externalTransactionToken: string; - products: string[]; -}`} - ), - swift: ( - {`// Android only - not available on iOS`} - ), - kotlin: ( - {`data class UserChoiceBillingDetails( - val externalTransactionToken: String, - val products: List -)`} - ), - kmp: ( - {`data class UserChoiceBillingDetails( - val externalTransactionToken: String, - val products: List -)`} - ), - dart: ( - {`class UserChoiceBillingDetails { - final String externalTransactionToken; - final List products; -}`} - ), - }} - -

    - externalTransactionToken - Token that must be - reported to Google Play within 24 hours -
    - products - List of product IDs selected by the user -

    - -

    Handling User Choice Billing

    -
      -
    1. - Receive UserChoiceBillingDetails via listener -
    2. -
    3. Process payment with your backend payment system
    4. -
    5. Send the external transaction token to your backend
    6. -
    7. - Backend reports token to Google Play within 24 hours (required for - compliance) -
    8. -
    9. Grant user access to purchased content
    10. -
    - -
    -

    - ⚠️ Important: The external transaction token MUST - be reported to Google Play within 24 hours. Failure to report tokens - may result in account suspension. It is strongly recommended to - handle token reporting on your backend server for reliability and - security. -

    -
    - -

    Flow Comparison

    -

    - When using User Choice Billing mode, there are two possible flows - depending on user selection: -

    -
      -
    • - Google Play selected - Standard{' '} - PurchaseUpdated event fires (handle normally) -
    • -
    • - Alternative billing selected -{' '} - UserChoiceBillingAndroid event fires (handle with your - payment system) -
    • -
    - -

    - See{' '} - - External Purchase documentation - {' '} - for complete implementation examples. -

    -
    - -
    - - Developer Provided Billing Event (Android 8.3.0+) - -

    - Fired when a user selects developer-provided billing in the External - Payments flow on Android. This is different from User Choice Billing - - it presents a side-by-side choice dialog in the purchase flow itself. -

    -

    - Note: Currently only available in Japan. -

    - -

    Listener Setup

    - - {{ - typescript: ( - {`developerProvidedBillingListener( - listener: (details: DeveloperProvidedBillingDetails) => void -): Subscription`} - ), - swift: ( - {`// Android only - not available on iOS`} - ), - kotlin: ( - {`// Callback approach -fun addDeveloperProvidedBillingListener( - listener: OpenIapDeveloperProvidedBillingListener -)`} - ), - kmp: ( - {`// Callback approach -fun addDeveloperProvidedBillingListener( - listener: OpenIapDeveloperProvidedBillingListener -)`} - ), - dart: ( - {`Stream get developerProvidedBillingStream; -// Android only (8.3.0+)`} - ), - }} - -

    - Registers a listener for Developer Provided Billing events. This - listener is only triggered when the user selects the developer's - payment option (instead of Google Play) in the External Payments flow. -

    - - - {{ - typescript: ( - {`import { developerProvidedBillingListener } from 'expo-iap'; - -const subscription = developerProvidedBillingListener(async (details) => { - console.log('User selected developer billing'); - console.log('Token:', details.externalTransactionToken); - - // Process payment with your payment system - const paymentResult = await processPaymentWithYourGateway({ - token: details.externalTransactionToken, - // Your payment details - }); - - if (paymentResult.success) { - // IMPORTANT: Report the token to Google Play within 24 hours - await reportExternalTransactionToGoogle(details.externalTransactionToken); - grantUserAccess(); - } -}); - -// Cleanup when done -subscription.remove();`} - ), - kotlin: ( - {`import dev.hyo.openiap.DeveloperProvidedBillingDetailsAndroid - -// Using callback -openIapStore.addDeveloperProvidedBillingListener { details -> - println("User selected developer billing") - println("Token: \${details.externalTransactionToken}") - - lifecycleScope.launch { - // Process payment with your payment system - val paymentResult = processPaymentWithYourGateway( - token = details.externalTransactionToken - ) - - if (paymentResult.success) { - // IMPORTANT: Report the token to Google Play within 24 hours - reportExternalTransactionToGoogle(details.externalTransactionToken) - grantUserAccess() - } - } -}`} - ), - kmp: ( - {`import io.github.hyochan.kmpiap.KmpIAP - -val kmpIAP = KmpIAP() - -// Using callback -kmpIAP.addDeveloperProvidedBillingListener { details -> - println("User selected developer billing") - println("Token: \${details.externalTransactionToken}") - - lifecycleScope.launch { - // Process payment with your payment system - val paymentResult = processPaymentWithYourGateway( - token = details.externalTransactionToken - ) - - if (paymentResult.success) { - // IMPORTANT: Report the token to Google Play within 24 hours - reportExternalTransactionToGoogle(details.externalTransactionToken) - grantUserAccess() - } - } -}`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -// Android only (8.3.0+) - will not fire on iOS or older Android -final subscription = FlutterInappPurchase.developerProvidedBillingStream - .listen((details) async { - print('User selected developer billing'); - print('Token: \${details.externalTransactionToken}'); - - // Process payment with your payment system - final paymentResult = await processPaymentWithYourGateway( - token: details.externalTransactionToken, - ); - - if (paymentResult.success) { - // IMPORTANT: Report the token to Google Play within 24 hours - await reportExternalTransactionToGoogle(details.externalTransactionToken); - grantUserAccess(); - } -}); - -// Cleanup when done -subscription.cancel();`} - ), - }} - - -

    Event Payload

    - - {{ - typescript: ( - {`interface DeveloperProvidedBillingDetails { - externalTransactionToken: string; -}`} - ), - swift: ( - {`// Android only - not available on iOS`} - ), - kotlin: ( - {`data class DeveloperProvidedBillingDetailsAndroid( - val externalTransactionToken: String -)`} - ), - kmp: ( - {`data class DeveloperProvidedBillingDetailsAndroid( - val externalTransactionToken: String -)`} - ), - dart: ( - {`class DeveloperProvidedBillingDetails { - final String externalTransactionToken; -}`} - ), - }} - -

    - externalTransactionToken - Token that must be - reported to Google Play within 24 hours after completing the payment -

    - -

    Comparison: User Choice vs Developer Provided Billing

    - - - + + - - - + + + + + + + + + + + +
    FeatureUser Choice BillingDeveloper Provided BillingListenerDescription
    Billing Library7.0+8.3.0+ + + purchaseUpdatedListener + + + Fires when a purchase is successful or a pending purchase is + completed. +
    + + purchaseErrorListener + + Fires when a purchase fails or is cancelled by the user.
    + + subscriptionBillingIssueListener + + + Fires when an active subscription enters a billing issue state + (iOS 18+ / Play Billing 8.1+; not emitted on Horizon). +
    +
    + +
    + + iOS Listeners + + + - - - + + + + - - - + + + +
    AvailabilityEligible regionsJapan onlyListenerDescription
    When presentedAfter initConnection()During requestPurchase() + + promotedProductListenerIOS + + + Fires when a user clicks on a promoted in-app purchase in the + App Store. +
    +
    + +
    + + Android Listeners + + + - - - + + + + - -
    UISeparate dialog before purchaseSide-by-side choice in purchase dialogListenerDescription
    Event - UserChoiceBillingAndroid + + userChoiceBillingListenerAndroid + - DeveloperProvidedBillingAndroid + Fires when a user selects alternative billing in the User Choice + Billing dialog.
    Setup - AlternativeBillingModeAndroid.UserChoice + + developerProvidedBillingListenerAndroid + - enableBillingProgram(EXTERNAL_PAYMENTS) +{' '} - developerBillingOption in requestPurchase + Fires when a user selects developer-provided billing in the + External Payments flow (8.3.0+, Japan only).
    - -
    -

    - ⚠️ Important: The external transaction token MUST - be reported to Google Play within 24 hours using the{' '} - externaltransactions.createexternaltransaction API. - Failure to report tokens may result in account suspension. -

    -
    - -

    - See{' '} - - External Payments documentation - {' '} - for complete implementation examples. -

    diff --git a/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx b/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx new file mode 100644 index 000000000..2d3febff6 --- /dev/null +++ b/packages/docs/src/pages/docs/events/android/developer-provided-billing-listener-android.tsx @@ -0,0 +1,277 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function DeveloperProvidedBillingListenerAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + developerProvidedBillingListenerAndroid +

    +

    + Fired when a user selects developer-provided billing in the External + Payments flow on Android. This is different from User Choice Billing - + it presents a side-by-side choice dialog in the purchase flow itself. +

    +

    + Note: Currently only available in Japan. +

    + +

    Listener Setup

    + + {{ + typescript: ( + {`developerProvidedBillingListenerAndroid( + listener: (details: DeveloperProvidedBillingDetailsAndroid) => void +): Subscription`} + ), + swift: ( + {`// Android only - not available on iOS`} + ), + kotlin: ( + {`// Callback approach +fun addDeveloperProvidedBillingListener( + listener: OpenIapDeveloperProvidedBillingListener +)`} + ), + kmp: ( + {`// Callback approach +fun addDeveloperProvidedBillingListener( + listener: OpenIapDeveloperProvidedBillingListener +)`} + ), + dart: ( + {`Stream get developerProvidedBillingStream; +// Android only (8.3.0+)`} + ), + }} + +

    + Registers a listener for Developer Provided Billing events. This + listener is only triggered when the user selects the developer's payment + option (instead of Google Play) in the External Payments flow. +

    + + + {{ + typescript: ( + {`import { developerProvidedBillingListenerAndroid } from 'expo-iap'; + +const subscription = developerProvidedBillingListenerAndroid(async (details) => { + console.log('User selected developer billing'); + console.log('Token:', details.externalTransactionToken); + + // Process payment with your payment system + const paymentResult = await processPaymentWithYourGateway({ + token: details.externalTransactionToken, + // Your payment details + }); + + if (paymentResult.success) { + // IMPORTANT: Report the token to Google Play within 24 hours + await reportExternalTransactionToGoogle(details.externalTransactionToken); + grantUserAccess(); + } +}); + +// Cleanup when done +subscription.remove();`} + ), + kotlin: ( + {`import dev.hyo.openiap.DeveloperProvidedBillingDetailsAndroid + +// Using callback +openIapStore.addDeveloperProvidedBillingListener { details -> + println("User selected developer billing") + println("Token: \${details.externalTransactionToken}") + + lifecycleScope.launch { + // Process payment with your payment system + val paymentResult = processPaymentWithYourGateway( + token = details.externalTransactionToken + ) + + if (paymentResult.success) { + // IMPORTANT: Report the token to Google Play within 24 hours + reportExternalTransactionToGoogle(details.externalTransactionToken) + grantUserAccess() + } + } +}`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() + +// Using callback +kmpIAP.addDeveloperProvidedBillingListener { details -> + println("User selected developer billing") + println("Token: \${details.externalTransactionToken}") + + lifecycleScope.launch { + // Process payment with your payment system + val paymentResult = processPaymentWithYourGateway( + token = details.externalTransactionToken + ) + + if (paymentResult.success) { + // IMPORTANT: Report the token to Google Play within 24 hours + reportExternalTransactionToGoogle(details.externalTransactionToken) + grantUserAccess() + } + } +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +// Android only (8.3.0+) - will not fire on iOS or older Android +final subscription = FlutterInappPurchase.developerProvidedBillingStream + .listen((details) async { + print('User selected developer billing'); + print('Token: \${details.externalTransactionToken}'); + + // Process payment with your payment system + final paymentResult = await processPaymentWithYourGateway( + token: details.externalTransactionToken, + ); + + if (paymentResult.success) { + // IMPORTANT: Report the token to Google Play within 24 hours + await reportExternalTransactionToGoogle(details.externalTransactionToken); + grantUserAccess(); + } +}); + +// Cleanup when done +subscription.cancel();`} + ), + }} + + +

    Event Payload

    + + {{ + typescript: ( + {`interface DeveloperProvidedBillingDetailsAndroid { + externalTransactionToken: string; +}`} + ), + swift: ( + {`// Android only - not available on iOS`} + ), + kotlin: ( + {`data class DeveloperProvidedBillingDetailsAndroid( + val externalTransactionToken: String +)`} + ), + kmp: ( + {`data class DeveloperProvidedBillingDetailsAndroid( + val externalTransactionToken: String +)`} + ), + dart: ( + {`class DeveloperProvidedBillingDetailsAndroid { + final String externalTransactionToken; +}`} + ), + }} + +

    + externalTransactionToken - Token that must be reported + to Google Play within 24 hours after completing the payment +

    + +

    Comparison: User Choice vs Developer Provided Billing

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FeatureUser Choice BillingDeveloper Provided Billing
    Billing Library7.0+8.3.0+
    AvailabilityEligible regionsJapan only
    When presentedAfter initConnection()During requestPurchase()
    UISeparate dialog before purchaseSide-by-side choice in purchase dialog
    Event + UserChoiceBillingAndroid + + DeveloperProvidedBillingAndroid +
    Setup + AlternativeBillingModeAndroid.UserChoice + + enableBillingProgram(EXTERNAL_PAYMENTS) +{' '} + developerBillingOption in requestPurchase +
    + +
    +

    + ⚠️ Important: The external transaction token MUST be + reported to Google Play within 24 hours using the{' '} + externaltransactions.createexternaltransaction API. + Failure to report tokens may result in account suspension. +

    +
    + +

    + See{' '} + + External Payments documentation + {' '} + for complete implementation examples. +

    +
    + ); +} + +export default DeveloperProvidedBillingListenerAndroid; diff --git a/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx b/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx new file mode 100644 index 000000000..0a3f0a4ae --- /dev/null +++ b/packages/docs/src/pages/docs/events/android/user-choice-billing-listener-android.tsx @@ -0,0 +1,266 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function UserChoiceBillingListenerAndroid() { + useScrollToHash(); + + return ( +
    + +

    + Android{' '} + userChoiceBillingListenerAndroid +

    +

    + Fired when a user selects alternative billing in the User Choice Billing + dialog on Android. +

    + +

    Listener Setup

    + + {{ + typescript: ( + {`userChoiceBillingListenerAndroid( + listener: (details: UserChoiceBillingDetails) => void +): Subscription`} + ), + swift: ( + {`// Android only - not available on iOS`} + ), + kotlin: ( + {`// Flow approach +val userChoiceBillingEvents: Flow`} + ), + kmp: ( + {`// Flow approach +val userChoiceBillingEvents: Flow`} + ), + dart: ( + {`Stream get userChoiceBillingStream; +// Android only`} + ), + }} + +

    + Registers a listener for User Choice Billing events. This listener is + only triggered when the user selects alternative billing instead of + Google Play billing. +

    + + + {{ + typescript: ( + {`import { userChoiceBillingListenerAndroid } from 'expo-iap'; + +const subscription = userChoiceBillingListenerAndroid(async (details) => { + console.log('User chose alternative billing'); + console.log('Products:', details.products); + console.log('Token:', details.externalTransactionToken); + + // Process payment with your backend + const paymentResult = await processPaymentWithBackend({ + products: details.products, + token: details.externalTransactionToken, + }); + + if (paymentResult.success) { + // Backend should report token to Google Play within 24 hours + grantUserAccess(details.products); + } +}); + +// Cleanup when done +subscription.remove();`} + ), + kotlin: ( + {`import dev.hyo.openiap.UserChoiceBillingDetails + +// Using Flow +lifecycleScope.launch { + openIapStore.userChoiceBillingEvents.collect { details -> + println("User chose alternative billing") + println("Products: \${details.products}") + println("Token: \${details.externalTransactionToken}") + + // Process payment with your backend + val paymentResult = processPaymentWithBackend( + products = details.products, + token = details.externalTransactionToken + ) + + if (paymentResult.success) { + // Backend should report token to Google Play within 24 hours + grantUserAccess(details.products) + } + } +} + +// Or with callback +openIapStore.setUserChoiceBillingListener { details -> + println("User chose alternative billing for: \${details.products}") +}`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() + +// Using Flow +lifecycleScope.launch { + kmpIAP.userChoiceBillingEvents.collect { details -> + println("User chose alternative billing") + println("Products: \${details.products}") + println("Token: \${details.externalTransactionToken}") + + // Process payment with your backend + val paymentResult = processPaymentWithBackend( + products = details.products, + token = details.externalTransactionToken + ) + + if (paymentResult.success) { + // Backend should report token to Google Play within 24 hours + grantUserAccess(details.products) + } + } +} + +// Or with callback +kmpIAP.setUserChoiceBillingListener { details -> + println("User chose alternative billing for: \${details.products}") +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +// Android only - will not fire on iOS +final subscription = FlutterInappPurchase.userChoiceBillingAndroid.listen((details) async { + print('User chose alternative billing'); + print('Products: \${details?.products}'); + print('Token: \${details?.externalTransactionToken}'); + + // Process payment with your backend + final paymentResult = await processPaymentWithBackend( + products: details!.products, + token: details.externalTransactionToken, + ); + + if (paymentResult.success) { + // Backend should report token to Google Play within 24 hours + grantUserAccess(details.products); + } +}); + +// Cleanup when done +subscription.cancel();`} + ), + }} + + +

    Event Payload

    + + {{ + typescript: ( + {`interface UserChoiceBillingDetails { + externalTransactionToken: string; + products: string[]; +}`} + ), + swift: ( + {`// Android only - not available on iOS`} + ), + kotlin: ( + {`data class UserChoiceBillingDetails( + val externalTransactionToken: String, + val products: List +)`} + ), + kmp: ( + {`data class UserChoiceBillingDetails( + val externalTransactionToken: String, + val products: List +)`} + ), + dart: ( + {`class UserChoiceBillingDetails { + final String externalTransactionToken; + final List products; +}`} + ), + }} + +

    + externalTransactionToken - Token that must be reported + to Google Play within 24 hours +
    + products - List of product IDs selected by the user +

    + +

    Handling User Choice Billing

    +
      +
    1. + Receive UserChoiceBillingDetails via listener +
    2. +
    3. Process payment with your backend payment system
    4. +
    5. Send the external transaction token to your backend
    6. +
    7. + Backend reports token to Google Play within 24 hours (required for + compliance) +
    8. +
    9. Grant user access to purchased content
    10. +
    + +
    +

    + ⚠️ Important: The external transaction token MUST be + reported to Google Play within 24 hours. Failure to report tokens may + result in account suspension. It is strongly recommended to handle + token reporting on your backend server for reliability and security. +

    +
    + +

    Flow Comparison

    +

    + When using User Choice Billing mode, there are two possible flows + depending on user selection: +

    +
      +
    • + Google Play selected - Standard{' '} + PurchaseUpdated event fires (handle normally) +
    • +
    • + Alternative billing selected -{' '} + UserChoiceBillingAndroid event fires (handle with your + payment system) +
    • +
    + +

    + See{' '} + + External Purchase documentation + {' '} + for complete implementation examples. +

    +
    + ); +} + +export default UserChoiceBillingListenerAndroid; 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 new file mode 100644 index 000000000..30c9063b1 --- /dev/null +++ b/packages/docs/src/pages/docs/events/ios/promoted-product-listener-ios.tsx @@ -0,0 +1,200 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function PromotedProductListenerIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + promotedProductListenerIOS +

    +

    + Fired when a user clicks on a promoted in-app purchase in the App Store. +

    + +

    Listener Setup

    + + {{ + typescript: ( + {`promotedProductListenerIOS( + listener: (productId: string) => void +): Subscription`} + ), + swift: ( + {`// AsyncSequence approach +var promotedProducts: AsyncStream + +// Combine approach +var promotedProductPublisher: AnyPublisher`} + ), + kotlin: ( + {`// iOS only - not available on Android`} + ), + kmp: ( + {`// iOS only - not available on Android`} + ), + dart: ( + {`Stream get promotedProductStream; // iOS only`} + ), + }} + +

    Registers a listener for App Store promoted product events.

    + + + {{ + typescript: ( + {`import { + promotedProductListenerIOS, + fetchProducts, + requestPurchase +} from 'expo-iap'; + +const subscription = promotedProductListenerIOS(async (productId) => { + console.log('Promoted product tapped:', productId); + + // Fetch product details + const products = await fetchProducts({ + skus: [productId], + type: 'in-app' + }); + + if (products.length > 0) { + // Show product info to user and confirm purchase + const confirmed = await showPurchaseConfirmation(products[0]); + + if (confirmed) { + // Purchase directly using requestPurchase with the received SKU + await requestPurchase({ + request: { apple: { sku: productId } }, + type: 'in-app' + }); + } + } +}); + +// Cleanup when done +subscription.remove();`} + ), + swift: ( + {`import OpenIap + +// Using async/await +Task { + for await productId in OpenIapModule.shared.promotedProducts { + print("Promoted product tapped: \\(productId)") + + // Fetch product details + let products = try await OpenIapModule.shared.fetchProducts( + ProductRequest(skus: [productId], type: .inApp) + ) + + if let product = products.first { + // Show product info to user and confirm purchase + if await showPurchaseConfirmation(product) { + // Purchase directly using requestPurchase with the received SKU + try await OpenIapModule.shared.requestPurchase( + RequestPurchaseProps( + request: .purchase(RequestPurchasePropsByPlatforms( + apple: RequestPurchaseIosProps(sku: productId) + )), + type: .inApp + ) + ) + } + } + } +} + +// Or using Combine +OpenIapModule.shared.promotedProductPublisher + .sink { productId in + print("Promoted product: \\(productId)") + } + .store(in: &cancellables)`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +// iOS only - will not fire on Android +final subscription = FlutterInappPurchase.promotedProductIOS.listen((productId) async { + print('Promoted product tapped: $productId'); + + // Fetch product details + final products = await FlutterInappPurchase.instance.fetchProducts( + ProductRequest(skus: [productId!], type: ProductQueryType.InApp), + ); + + if (products.isNotEmpty) { + // Show product info to user and confirm purchase + final confirmed = await showPurchaseConfirmation(products.first); + + if (confirmed) { + // Purchase directly using requestPurchase with the received SKU + await FlutterInappPurchase.instance.requestPurchase( + RequestPurchaseProps( + request: RequestPurchasePropsByPlatforms( + apple: RequestPurchaseIosProps(sku: productId!), + ), + type: ProductQueryType.InApp, + ), + ); + } + } +}); + +// Cleanup when done +subscription.cancel();`} + ), + }} + + +

    Handling Promoted Products

    +
      +
    1. Receive product SKU via listener
    2. +
    3. + Fetch product details using{' '} + fetchProducts +
    4. +
    5. Display product information to user
    6. +
    7. + Call requestPurchase{' '} + with the received SKU if user confirms +
    8. +
    +

    + Also check{' '} + + getPromotedProductIOS + {' '} + on app launch for pending promoted products. +

    +
    +

    + Note: In StoreKit 2, promoted products can be + purchased directly via the standard{' '} + + requestPurchase() + {' '} + flow. The deprecated{' '} + + requestPurchaseOnPromotedProductIOS() + {' '} + API is no longer needed. +

    +
    +
    + ); +} + +export default PromotedProductListenerIOS; diff --git a/packages/docs/src/pages/docs/events/purchase-error-listener.tsx b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx new file mode 100644 index 000000000..1fbb0dba4 --- /dev/null +++ b/packages/docs/src/pages/docs/events/purchase-error-listener.tsx @@ -0,0 +1,238 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function PurchaseErrorListener() { + useScrollToHash(); + + return ( +
    + +

    purchaseErrorListener

    +

    Fired when a purchase fails or is cancelled by the user.

    + +

    Listener Setup

    + + {{ + typescript: ( + {`purchaseErrorListener( + listener: (error: PurchaseError) => void +): Subscription`} + ), + swift: ( + {`// AsyncSequence approach +var purchaseErrors: AsyncStream + +// Combine approach +var purchaseErrorPublisher: AnyPublisher`} + ), + kotlin: ( + {`// Flow approach +val purchaseErrors: Flow`} + ), + kmp: ( + {`// Flow approach +val purchaseErrors: Flow`} + ), + dart: ( + {`Stream get purchaseErrorStream;`} + ), + }} + +

    Registers a listener for purchase error events.

    + + + {{ + typescript: ( + {`import { + purchaseErrorListener, + ErrorCode, + restorePurchases, +} from 'expo-iap'; +// showRetryDialog / showErrorMessage are user-defined UI helpers. + +const subscription = purchaseErrorListener((error) => { + console.log('Purchase error:', error.code, error.message); + + switch (error.code) { + case ErrorCode.UserCancelled: + // User cancelled - no action needed + break; + case ErrorCode.AlreadyOwned: + // Restore purchases instead + restorePurchases(); + break; + case ErrorCode.NetworkError: + // Show retry option + showRetryDialog(); + break; + default: + showErrorMessage(error.message); + } +}); + +// Cleanup when done +subscription.remove();`} + ), + swift: ( + {`import OpenIap + +// Using async/await +Task { + for await error in OpenIapModule.shared.purchaseErrors { + print("Purchase error: \\(error.code) - \\(error.message)") + + switch error.code { + case .userCancelled: + // User cancelled - no action needed + break + case .alreadyOwned: + // Restore purchases instead + try await OpenIapModule.shared.restorePurchases() + case .networkError: + showRetryDialog() + default: + showErrorMessage(error.message) + } + } +} + +// Or using Combine +OpenIapModule.shared.purchaseErrorPublisher + .sink { error in + print("Purchase error: \\(error.code)") + } + .store(in: &cancellables)`} + ), + kotlin: ( + {`import dev.hyo.openiap.OpenIapError + +// Using Flow +lifecycleScope.launch { + openIapStore.purchaseErrors.collect { error -> + println("Purchase error: \${error.code} - \${error.message}") + + when (error.code) { + OpenIapError.UserCancelled -> { + // User cancelled - no action needed + } + OpenIapError.AlreadyOwned -> { + // Restore purchases instead + openIapStore.restorePurchases() + } + OpenIapError.NetworkError -> { + showRetryDialog() + } + else -> { + showErrorMessage(error.message) + } + } + } +} + +// Or with callback +openIapStore.setPurchaseErrorListener { error -> + println("Purchase error: \${error.code}") +}`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() + +// Using Flow +lifecycleScope.launch { + kmpIAP.purchaseErrors.collect { error -> + println("Purchase error: \${error.code} - \${error.message}") + + when (error.code) { + OpenIapError.UserCancelled -> { + // User cancelled - no action needed + } + OpenIapError.AlreadyOwned -> { + // Restore purchases instead + kmpIAP.restorePurchases() + } + OpenIapError.NetworkError -> { + showRetryDialog() + } + else -> { + showErrorMessage(error.message) + } + } + } +} + +// Or with callback +kmpIAP.setPurchaseErrorListener { error -> + println("Purchase error: \${error.code}") +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +final subscription = FlutterInappPurchase.purchaseError.listen((error) { + print('Purchase error: \${error?.code} - \${error?.message}'); + + switch (error?.code) { + case 'E_USER_CANCELLED': + // User cancelled - no action needed + break; + case 'E_ALREADY_OWNED': + // Restore purchases instead + FlutterInappPurchase.instance.restorePurchases(); + break; + case 'E_NETWORK_ERROR': + showRetryDialog(); + break; + default: + showErrorMessage(error?.message ?? 'Unknown error'); + } +}); + +// Cleanup when done +subscription.cancel();`} + ), + }} + + +

    Error Payload

    +

    + The error event delivers a PurchaseError{' '} + object with error details. See{' '} + Error Codes for complete reference. +

    + +

    Error Handling Strategy

    +

    + Handle errors based on their error codes: +

    +
      +
    • + UserCancelled - No action required +
    • +
    • + ItemUnavailable - Check product availability +
    • +
    • + NetworkError - Retry with backoff +
    • +
    • + AlreadyOwned - Restore purchases +
    • +
    • + ReceiptFailed - Retry validation +
    • +
    +
    + ); +} + +export default PurchaseErrorListener; diff --git a/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx b/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx new file mode 100644 index 000000000..2ee9dda0e --- /dev/null +++ b/packages/docs/src/pages/docs/events/purchase-updated-listener.tsx @@ -0,0 +1,220 @@ +import { Link } from 'react-router-dom'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function PurchaseUpdatedListener() { + useScrollToHash(); + + return ( +
    + +

    purchaseUpdatedListener

    +

    + Fired when a purchase is successful or when a pending purchase is + completed. +

    + +

    Listener Setup

    + + {{ + typescript: ( + {`purchaseUpdatedListener( + listener: (purchase: Purchase) => void +): Subscription`} + ), + swift: ( + {`// AsyncSequence approach +var purchaseUpdates: AsyncStream + +// Combine approach +var purchaseUpdatedPublisher: AnyPublisher`} + ), + kotlin: ( + {`// Flow approach +val purchaseUpdates: Flow`} + ), + kmp: ( + {`// Flow approach +val purchaseUpdates: Flow`} + ), + dart: ( + {`Stream get purchaseUpdatedStream;`} + ), + gdscript: ( + {`signal purchase_updated(purchase: Purchase)`} + ), + }} + +

    Registers a listener for successful purchase events.

    + + + {{ + typescript: ( + {`import { finishTransaction, purchaseUpdatedListener } from 'expo-iap'; +// Same API in react-native-iap: +// import { finishTransaction, purchaseUpdatedListener } from 'react-native-iap'; + +const subscription = purchaseUpdatedListener(async (purchase) => { + console.log('Purchase updated:', purchase.productId); + + // Validate the receipt + const isValid = await validateReceipt(purchase); + + if (isValid) { + // Deliver content to user + await deliverProduct(purchase.productId); + + // Finish the transaction + await finishTransaction({ purchase, isConsumable: false }); + } +}); + +// Cleanup when done +subscription.remove();`} + ), + swift: ( + {`import OpenIap + +// Using async/await +Task { + for await purchase in OpenIapModule.shared.purchaseUpdates { + print("Purchase updated: \\(purchase.productId)") + + // Validate and deliver + if await validateReceipt(purchase) { + await deliverProduct(purchase.productId) + try await OpenIapModule.shared.finishTransaction(purchase) + } + } +} + +// Or using Combine +OpenIapModule.shared.purchaseUpdatedPublisher + .sink { purchase in + print("Purchase updated: \\(purchase.productId)") + } + .store(in: &cancellables)`} + ), + kotlin: ( + {`import dev.hyo.openiap.OpenIapStore + +// Using Flow +lifecycleScope.launch { + openIapStore.purchaseUpdates.collect { purchase -> + println("Purchase updated: \${purchase.productId}") + + // Validate and deliver + if (validateReceipt(purchase)) { + deliverProduct(purchase.productId) + openIapStore.finishTransaction(purchase, isConsumable = false) + } + } +} + +// Or with callback +openIapStore.setPurchaseUpdatedListener { purchase -> + println("Purchase updated: \${purchase.productId}") +}`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() + +// Using Flow +lifecycleScope.launch { + kmpIAP.purchaseUpdates.collect { purchase -> + println("Purchase updated: \${purchase.productId}") + + // Validate and deliver + if (validateReceipt(purchase)) { + deliverProduct(purchase.productId) + kmpIAP.finishTransaction(purchase, isConsumable = false) + } + } +} + +// Or with callback +kmpIAP.setPurchaseUpdatedListener { purchase -> + println("Purchase updated: \${purchase.productId}") +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +final subscription = FlutterInappPurchase.purchaseUpdated.listen((purchase) async { + print('Purchase updated: \${purchase?.productId}'); + + // Validate the receipt + final isValid = await validateReceipt(purchase); + + if (isValid) { + // Deliver content to user + await deliverProduct(purchase!.productId); + + // Finish the transaction + await FlutterInappPurchase.instance.finishTransaction(purchase); + } +}); + +// Cleanup when done +subscription.cancel();`} + ), + gdscript: ( + {`# Connect to the signal +iap.purchase_updated.connect(_on_purchase_updated) + +func _on_purchase_updated(purchase: Purchase): + print("Purchase updated: %s" % purchase.product_id) + + # Validate the receipt + var is_valid = await validate_receipt(purchase) + + if is_valid: + # Deliver content to user + await deliver_product(purchase.product_id) + + # Finish the transaction + await iap.finish_transaction(purchase, false) + +# Cleanup when done +func _exit_tree(): + iap.purchase_updated.disconnect(_on_purchase_updated)`} + ), + }} + + +

    Event Payload

    +

    + The purchase event delivers a{' '} + Purchase object containing + transaction details. +

    + +

    Purchase Update Flow

    +
      +
    1. + Receive Purchase object via + listener +
    2. +
    3. Validate receipt with backend service
    4. +
    5. Deliver purchased content to user
    6. +
    7. + Finish transaction with{' '} + finishTransaction{' '} + (handles acknowledgment on both platforms) +
    8. +
    9. Update application state
    10. +
    +
    + ); +} + +export default PurchaseUpdatedListener; diff --git a/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx new file mode 100644 index 000000000..eb88ec3e4 --- /dev/null +++ b/packages/docs/src/pages/docs/events/subscription-billing-issue-listener.tsx @@ -0,0 +1,185 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function SubscriptionBillingIssueListener() { + useScrollToHash(); + + return ( +
    + +

    subscriptionBillingIssueListener

    +

    + Fired when an active subscription enters a state that needs user + attention because of a payment problem — card declined, expired payment + method, billing retry, or grace period. Unifies StoreKit 2{' '} + Message.billingIssue (iOS 18+) and Play Billing{' '} + Purchase.isSuspended (Play Billing Library 8.1+) under a + single cross-platform stream. Silent no-op on platforms that cannot emit + (tvOS, watchOS, visionOS, macOS, Meta Horizon). +

    + + + Listener Setup + + + {{ + typescript: ( + {`subscriptionBillingIssueListener( + listener: (purchase: Purchase) => void +): Subscription`} + ), + swift: ( + {`// Callback + Subscription handle (iOS 18+ only) +func subscriptionBillingIssueListener( + _ listener: @escaping @Sendable (Purchase) -> Void +) -> Subscription`} + ), + kotlin: ( + {`// Callback approach (Play Billing 8.1+) +fun addSubscriptionBillingIssueListener( + listener: OpenIapSubscriptionBillingIssueListener +)`} + ), + kmp: ( + {`// Flow approach +val subscriptionBillingIssueListener: Flow`} + ), + dart: ( + {`Stream get subscriptionBillingIssueListener;`} + ), + gdscript: ( + {`signal subscription_billing_issue(purchase: Purchase)`} + ), + }} + +

    + The emitted{' '} + + Purchase + {' '} + is a regular subscription payload — use productId,{' '} + purchaseToken, and platform fields to prompt the user to + update payment. Play deduplicates by purchaseToken per + session; iOS fires per Message delivery. +

    + + + Example + + + {{ + typescript: ( + {`// expo-iap +import { subscriptionBillingIssueListener } from 'expo-iap'; +// Same API in react-native-iap: +// import { subscriptionBillingIssueListener } from 'react-native-iap'; + +const subscription = subscriptionBillingIssueListener((purchase) => { + console.log('Billing issue on', purchase.productId); + // Surface a "Update payment method" prompt and link the user to + // the platform's subscription management UI. + showBillingIssueBanner(purchase); +}); + +// Cleanup when the screen unmounts +subscription.remove(); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +import { useIAP } from 'expo-iap'; + +function BillingIssueGate() { + useIAP({ + onSubscriptionBillingIssue: (purchase) => { + showBillingIssueBanner(purchase); + }, + }); + return null; +}`} + ), + swift: ( + {`import OpenIap + +// iOS 18+ only — no-op on older versions +let subscription = OpenIapModule.shared.subscriptionBillingIssueListener { purchase in + print("Billing issue on \\(purchase.productId)") + Task { await showBillingIssueBanner(purchase) } +} + +// Cleanup when the view disappears +subscription.remove()`} + ), + kotlin: ( + {`import dev.hyo.openiap.OpenIapStore + +val openIapStore = OpenIapStore(context) + +// Play Billing Library 8.1+ +val listener: (Purchase) -> Unit = { purchase -> + println("Billing issue on \${purchase.productId}") + showBillingIssueBanner(purchase) +} +openIapStore.addSubscriptionBillingIssueListener(listener) + +// Cleanup when the view disappears +openIapStore.removeSubscriptionBillingIssueListener(listener)`} + ), + kmp: ( + {`import io.github.hyochan.kmpiap.KmpIAP + +val kmpIAP = KmpIAP() + +// Play Billing 8.1+ on Android, iOS 18+ on Apple targets +lifecycleScope.launch { + kmpIAP.subscriptionBillingIssueListener.collect { purchase -> + println("Billing issue on \${purchase.productId}") + showBillingIssueBanner(purchase) + } +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +final subscription = + FlutterInappPurchase.subscriptionBillingIssueListener.listen((purchase) { + debugPrint('Billing issue on \${purchase.productId}'); + showBillingIssueBanner(purchase); + }); + +// Cleanup when the widget disposes +subscription.cancel();`} + ), + gdscript: ( + {`iap.subscription_billing_issue.connect(_on_billing_issue) + +func _on_billing_issue(purchase: Purchase): + print("Billing issue on %s" % purchase.product_id) + show_billing_issue_banner(purchase) + +# Cleanup when leaving the scene +func _exit_tree(): + iap.subscription_billing_issue.disconnect(_on_billing_issue)`} + ), + }} + + +

    + See{' '} + + Subscription Billing Issue feature guide + {' '} + for platform coverage, signal sources, and UX recommendations. +

    +
    + ); +} + +export default SubscriptionBillingIssueListener; diff --git a/packages/docs/src/pages/docs/features/alternative-marketplace/index.tsx b/packages/docs/src/pages/docs/features/alternative-marketplace/index.tsx index b5458fd8d..796caf368 100644 --- a/packages/docs/src/pages/docs/features/alternative-marketplace/index.tsx +++ b/packages/docs/src/pages/docs/features/alternative-marketplace/index.tsx @@ -125,6 +125,34 @@ function AlternativeMarketplace() {
    + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/features/alternative-marketplace/onside.tsx b/packages/docs/src/pages/docs/features/alternative-marketplace/onside.tsx index f9fb9bd00..43ef3d782 100644 --- a/packages/docs/src/pages/docs/features/alternative-marketplace/onside.tsx +++ b/packages/docs/src/pages/docs/features/alternative-marketplace/onside.tsx @@ -236,6 +236,34 @@ function Store() { + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/apis/debugging.tsx b/packages/docs/src/pages/docs/features/debugging.tsx similarity index 75% rename from packages/docs/src/pages/docs/apis/debugging.tsx rename to packages/docs/src/pages/docs/features/debugging.tsx index 266da6919..42f35777a 100644 --- a/packages/docs/src/pages/docs/apis/debugging.tsx +++ b/packages/docs/src/pages/docs/features/debugging.tsx @@ -7,16 +7,16 @@ import SEO from '../../../components/SEO'; import TLDRBox from '../../../components/TLDRBox'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; -function DebuggingAPIs() { +function Debugging() { useScrollToHash(); return (

    Debugging & Logging

    @@ -94,11 +94,18 @@ OpenIapLog.enable(false)`}

    Root Cause

    - Google Play Billing API's Purchase object does NOT - include basePlanId information. When a subscription group - has multiple base plans (weekly, monthly, yearly), there is no way to - determine which specific plan was purchased from the client-side{' '} - Purchase object. + Google Play Billing API's{' '} + + Purchase + {' '} + object does NOT include basePlanId information. When a + subscription group has multiple base plans (weekly, monthly, yearly), + there is no way to determine which specific plan was purchased from + the client-side{' '} + + Purchase + {' '} + object.

    @@ -211,8 +218,11 @@ console.log('Actual basePlanId:', basePlanId);`}

    Note: This is a fundamental limitation of Google Play Billing API, not a bug in this library. The{' '} - Purchase object from Google simply does not include{' '} - basePlanId information. + + Purchase + {' '} + object from Google simply does not include basePlanId{' '} + information.

    @@ -252,9 +262,7 @@ console.log('Actual basePlanId:', basePlanId);`} IAP operation called before initConnection() Call{' '} - - initConnection() - {' '} + initConnection(){' '} first @@ -265,7 +273,7 @@ console.log('Actual basePlanId:', basePlanId);`} Purchase completed but finishTransaction not called Call{' '} - + finishTransaction() {' '} after verification @@ -274,8 +282,56 @@ console.log('Actual basePlanId:', basePlanId);`} + +
    + + Native References + + +
    ); } -export default DebuggingAPIs; +export default Debugging; diff --git a/packages/docs/src/pages/docs/features/discount.tsx b/packages/docs/src/pages/docs/features/discount.tsx index 2bdd0fd51..8a6b8c217 100644 --- a/packages/docs/src/pages/docs/features/discount.tsx +++ b/packages/docs/src/pages/docs/features/discount.tsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; import CodeBlock from '../../../components/CodeBlock'; import LanguageTabs from '../../../components/LanguageTabs'; @@ -33,10 +34,9 @@ function Discount() {

    Standardized Types: For cross-platform development, - use the new{' '} - DiscountOffer type - which provides a unified interface with platform-specific fields via - suffixes (e.g., offerTokenAndroid). + use the new DiscountOffer{' '} + type which provides a unified interface with platform-specific fields + via suffixes (e.g., offerTokenAndroid).

    @@ -1261,6 +1261,44 @@ async function purchaseWithOffer( + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/features/external-purchase.tsx b/packages/docs/src/pages/docs/features/external-purchase.tsx index 82906034c..48cbc92d1 100644 --- a/packages/docs/src/pages/docs/features/external-purchase.tsx +++ b/packages/docs/src/pages/docs/features/external-purchase.tsx @@ -52,7 +52,11 @@ function ExternalPurchase() { iOS External Purchase URL - iOS 17.4+ (Notice Sheet), iOS 18.2+ (New APIs) + + iOS 17.4+ (Notice Sheet) +
    + iOS 18.2+ (New APIs) + StoreKit 2 @@ -1703,7 +1707,9 @@ func handle_external_purchase_with_billing_programs(product_id: String) -> void: -

    External Payments (8.3.0+ - Japan Only)

    +

    + External Payments (8.3.0+ - Japan Only) +

    Google Play Billing Library 8.3.0 introduces the{' '} External Payments program, currently @@ -2432,7 +2438,9 @@ func _ready_user_choice() -> void: 1 - canPresentExternalPurchaseNoticeIOS() + + canPresentExternalPurchaseNoticeIOS() + Check if device supports external purchase notice sheet @@ -2441,7 +2449,9 @@ func _ready_user_choice() -> void: 2 - presentExternalPurchaseNoticeSheetIOS() + + presentExternalPurchaseNoticeSheetIOS() + Show Apple's notice sheet informing user about external @@ -2791,25 +2801,25 @@ func _ready_user_choice() -> void:

    • - + External Purchase Types {' '} - Type definitions and parameters
    • - - Alternative Billing Example + + Alternative Marketplace {' '} - - Complete React Native example + - Onside & alternative billing flows
    • - - Alternative Billing Guide + + Alternative Billing Types {' '} - - Setup and configuration guide + - Type definitions and config
    • - Request Purchase API - + Request Purchase API - API reference for requestPurchase
    • @@ -2818,6 +2828,65 @@ func _ready_user_choice() -> void:
    + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx index 65e3a4029..9bbfafb90 100644 --- a/packages/docs/src/pages/docs/features/offer-code-redemption.tsx +++ b/packages/docs/src/pages/docs/features/offer-code-redemption.tsx @@ -79,7 +79,9 @@ function OfferCodeRedemption() {
  • Handled entirely by the iOS system
  • Purchase updates delivered through{' '} - purchaseUpdatedListener + + purchaseUpdatedListener +
  • @@ -446,7 +448,9 @@ func redeem_code() -> void:
  • User enters code in the Play Store
  • Purchase updates delivered through{' '} - purchaseUpdatedListener + + purchaseUpdatedListener +
  • @@ -567,7 +571,7 @@ func redeem_with_code(code: String) -> void:
    • - + presentCodeRedemptionSheetIOS API Reference
    • @@ -579,6 +583,45 @@ func redeem_with_code(code: String) -> void:
    + +
    + + Native References + + +
    ); } diff --git a/packages/docs/src/pages/docs/features/purchase.tsx b/packages/docs/src/pages/docs/features/purchase.tsx index d7aa8801a..d0eb14984 100644 --- a/packages/docs/src/pages/docs/features/purchase.tsx +++ b/packages/docs/src/pages/docs/features/purchase.tsx @@ -80,7 +80,8 @@ function Purchase() { {{ typescript: ( - {`import { useEffect, useCallback } from 'react'; + {`// expo-iap +import { useEffect, useCallback } from 'react'; import { initConnection, endConnection, @@ -89,6 +90,15 @@ import { type Purchase, type PurchaseError, } from 'expo-iap'; +// Same API in react-native-iap: +// import { +// initConnection, +// endConnection, +// purchaseUpdatedListener, +// purchaseErrorListener, +// type Purchase, +// type PurchaseError, +// } from 'react-native-iap'; function App() { useEffect(() => { @@ -123,6 +133,24 @@ function App() { }; }, []); + return ; +} + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +// useIAP handles connection, listener wiring, and cleanup for you. Pass +// onPurchaseSuccess / onPurchaseError to receive the same callbacks. +import { useIAP } from 'expo-iap'; + +function AppWithHook() { + useIAP({ + onPurchaseSuccess: (purchase) => { + void handlePurchase(purchase); + }, + onPurchaseError: (error) => { + handlePurchaseError(error); + }, + }); + return ; }`} ), @@ -354,9 +382,17 @@ func _exit_tree() -> void: request are event-based operations, not promise-based. Do not rely on their return values for actual purchase results — instead, listen for events through{' '} - purchaseUpdatedListener or{' '} - purchaseErrorListener. See{' '} - API Terminology{' '} + + purchaseUpdatedListener + {' '} + or{' '} + + purchaseErrorListener + + . See{' '} + + API Terminology + {' '} for details.

    @@ -367,7 +403,10 @@ func _exit_tree() -> void: {{ typescript: ( - {`import { requestPurchase } from 'expo-iap'; + {`// expo-iap +import { requestPurchase } from 'expo-iap'; +// Same API in react-native-iap: +// import { requestPurchase } from 'react-native-iap'; // Purchase a one-time product (consumable or non-consumable) const purchaseProduct = async (productId: string) => { @@ -377,7 +416,7 @@ const purchaseProduct = async (productId: string) => { apple: { sku: productId }, google: { skus: [productId] }, }, - type: 'inapp', // 'inapp' for consumables/non-consumables + type: 'in-app', // 'in-app' for consumables/non-consumables }); // Purchase result will be delivered to purchaseUpdatedListener } catch (error) { @@ -386,7 +425,29 @@ const purchaseProduct = async (productId: string) => { }; // Example usage -await purchaseProduct('com.app.coins_100');`} +await purchaseProduct('com.app.coins_100'); + +// --- Or via the useIAP() hook (also exported from react-native-iap) --- +import { useIAP } from 'expo-iap'; + +function BuyButton({ productId }: { productId: string }) { + const { requestPurchase } = useIAP(); + + return ( + - - ), - { icon: '⬇️' } - ); - }; + navigate(redirect, { replace: true }); + }, [location.hash, location.pathname, navigate]); return (
    -
    -

    Types

    -
    - - -
    -
    - +

    Types

    - Complete type definitions for OpenIAP. Types are organized by category - to help you find what you need quickly. + Complete type reference for OpenIAP. Each type below has its own page + with field definitions, examples, and related links.

    - - - +
    + + Common + + +
    + +
    + + Validation + + +
    -

    Core Types

    -

    Essential types used in every IAP implementation.

    -
    - - - - -
    + + Alternative Billing + +
    -

    Advanced Types

    -

    Types for billing options and purchase verification.

    -
    - - -
    + + iOS Specific + +
    -

    Platform-Specific Types

    -

    - Types specific to iOS and Android platforms. Note: Discount/offer - types have been standardized in{' '} - Offer Types. -

    -
    - - -
    + + Android Specific + +
    ); diff --git a/packages/docs/src/pages/docs/types/ios.tsx b/packages/docs/src/pages/docs/types/ios.tsx deleted file mode 100644 index b81f4c0a2..000000000 --- a/packages/docs/src/pages/docs/types/ios.tsx +++ /dev/null @@ -1,685 +0,0 @@ -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function TypesIOS() { - useScrollToHash(); - - return ( -
    - -

    iOS Types

    -

    - Type definitions specific to iOS/StoreKit 2 for discounts, offers, - subscription status, and app transactions. -

    - - - - - -
    -

    - Deprecation Notice: The iOS-specific discount and - offer types (DiscountOfferIOS, DiscountIOS,{' '} - SubscriptionOfferIOS) are deprecated. Use the new - cross-platform{' '} - DiscountOffer and SubscriptionOffer{' '} - types instead. -

    -
    - -
    - - DiscountOfferIOS Deprecated - -

    - Deprecated: Use{' '} - SubscriptionOffer{' '} - instead. -

    -

    - Used when requesting a purchase with a promotional offer. Generate - signature server-side. -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - identifier - Discount identifier from App Store Connect
    - keyIdentifier - Key ID for signature validation
    - nonce - Cryptographic nonce (UUID)
    - signature - Server-generated signature
    - timestamp - Timestamp when signature was generated
    -
    - -
    - - DiscountIOS Deprecated - -

    - Deprecated: Use{' '} - SubscriptionOffer{' '} - instead. -

    -

    Discount info returned as part of product details:

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - identifier - Discount identifier
    - type - Discount type (introductory, promotional)
    - numberOfPeriods - Number of billing periods
    - price - Formatted price string
    - priceAmount - Numeric price value
    - paymentMode - Payment mode (FreeTrial, PayAsYouGo, PayUpFront)
    - subscriptionPeriod - Period duration string
    -
    - -
    - - SubscriptionPeriodIOS - -

    Subscription period units:

    - - - - - - - - - - - - - -
    NameSummary
    - Day, Week, Month,{' '} - Year - Available subscription period units
    -
    - -
    - - PaymentMode - -

    Payment mode for offers:

    - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - FreeTrial - Free trial period
    - PayAsYouGo - Pay each period at reduced price
    - PayUpFront - Pay full amount upfront
    -
    - -
    - - SubscriptionStatusIOS - -

    - Subscription status from StoreKit 2. Use{' '} - subscriptionStatusIOS(sku) to get detailed subscription - state. -

    - - - - - - - - - - - - - - - - - -
    NameSummary
    - state - Current renewal state (see values below)
    - renewalInfo - - Renewal details. Contains: willAutoRenew,{' '} - autoRenewPreference -
    - - - Subscription State Values - -

    - The state field indicates the current subscription - status: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    StateDescriptionUser Access
    - subscribed - Active subscriptionGrant access
    - expired - Subscription has expiredDeny access
    - revoked - Refunded by AppleDeny access
    - inGracePeriod - Billing failed but grace period activeGrant access (temporary)
    - inBillingRetryPeriod - Billing retry in progressConsider granting access
    - - - iOS Expiration Reasons - -

    - When willAutoRenew is false, the{' '} - expirationReason field in renewalInfo{' '} - indicates why: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ReasonDescription
    - VOLUNTARY - User cancelled the subscription
    - BILLING_ERROR - Payment failed (card declined, etc.)
    - DID_NOT_AGREE_TO_PRICE_INCREASE - User declined a price increase
    - PRODUCT_NOT_AVAILABLE - Product no longer available for purchase
    - UNKNOWN - Unknown reason
    -
    - -
    - - AppTransaction - -

    - Represents the app transaction information returned by{' '} - getAppTransactionIOS(). Contains metadata about the - app's purchase and installation. -

    - - - Fields - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - bundleId - App bundle identifier
    - appVersion - Current app version
    - originalAppVersion - Version when user originally purchased/downloaded
    - originalPurchaseDate - Original purchase timestamp
    - deviceVerification - Device verification data
    - deviceVerificationNonce - Nonce for device verification
    - environment - - Environment: "Sandbox" or "Production" -
    - signedDate - Date when the transaction was signed
    - appId - App ID number
    - appVersionId - App version ID number
    - preorderDate - Preorder date (optional)
    - appTransactionId - App transaction ID (iOS 18.4+)
    - originalPlatform - Original platform (iOS 18.4+)
    - - - Type Definition - - - {{ - typescript: ( - {`interface AppTransaction { - bundleId: string; - appVersion: string; - originalAppVersion: string; - originalPurchaseDate: number; // epoch ms - deviceVerification: string; - deviceVerificationNonce: string; - environment: 'Sandbox' | 'Production'; - signedDate: number; // epoch ms - appId: number; - appVersionId: number; - preorderDate?: number; // epoch ms - // iOS 18.4+ properties - appTransactionId?: string; - originalPlatform?: string; -}`} - ), - swift: ( - {`struct AppTransaction { - let bundleId: String - let appVersion: String - let originalAppVersion: String - let originalPurchaseDate: Date - let deviceVerification: String - let deviceVerificationNonce: String - let environment: String // "Sandbox" | "Production" - let signedDate: Date - let appId: Int - let appVersionId: Int - let preorderDate: Date? - // iOS 18.4+ properties - let appTransactionId: String? - let originalPlatform: String? -}`} - ), - kotlin: ( - {`data class AppTransaction( - val bundleId: String, - val appVersion: String, - val originalAppVersion: String, - val originalPurchaseDate: Long, // epoch ms - val deviceVerification: String, - val deviceVerificationNonce: String, - val environment: String, // "Sandbox" | "Production" - val signedDate: Long, // epoch ms - val appId: Long, - val appVersionId: Long, - val preorderDate: Long? = null, - // iOS 18.4+ properties - val appTransactionId: String? = null, - val originalPlatform: String? = null -)`} - ), - dart: ( - {`class AppTransaction { - final String bundleId; - final String appVersion; - final String originalAppVersion; - final int originalPurchaseDate; // epoch ms - final String deviceVerification; - final String deviceVerificationNonce; - final String environment; // "Sandbox" | "Production" - final int signedDate; // epoch ms - final int appId; - final int appVersionId; - final int? preorderDate; - // iOS 18.4+ properties - final String? appTransactionId; - final String? originalPlatform; - - AppTransaction({ - required this.bundleId, - required this.appVersion, - required this.originalAppVersion, - required this.originalPurchaseDate, - required this.deviceVerification, - required this.deviceVerificationNonce, - required this.environment, - required this.signedDate, - required this.appId, - required this.appVersionId, - this.preorderDate, - this.appTransactionId, - this.originalPlatform, - }); -}`} - ), - gdscript: ( - {`class_name AppTransaction - -var bundle_id: String -var app_version: String -var original_app_version: String -var original_purchase_date: int # epoch ms -var device_verification: String -var device_verification_nonce: String -var environment: String # "Sandbox" | "Production" -var signed_date: int # epoch ms -var app_id: int -var app_version_id: int -var preorder_date: int # optional, epoch ms -# iOS 18.4+ properties -var app_transaction_id: String # optional -var original_platform: String # optional`} - ), - }} - - - - Usage Example - - - {{ - typescript: ( - {`import { getAppTransactionIOS } from 'expo-iap'; - -// Get app transaction (iOS only) -const appTransaction = await getAppTransactionIOS(); - -if (appTransaction) { - console.log('Bundle ID:', appTransaction.bundleId); - console.log('Original version:', appTransaction.originalAppVersion); - console.log('Environment:', appTransaction.environment); - - // Check if user originally purchased on a different platform (iOS 18.4+) - if (appTransaction.originalPlatform) { - console.log('Originally purchased on:', appTransaction.originalPlatform); - } -}`} - ), - swift: ( - {`import OpenIap - -// Get app transaction (iOS only) -let appTransaction = try await OpenIapModule.shared.getAppTransactionIOS() - -if let transaction = appTransaction { - print("Bundle ID: \\(transaction.bundleId)") - print("Original version: \\(transaction.originalAppVersion)") - print("Environment: \\(transaction.environment)") - - // Check if user originally purchased on a different platform (iOS 18.4+) - if let platform = transaction.originalPlatform { - print("Originally purchased on: \\(platform)") - } -}`} - ), - kotlin: ( - {`import io.github.hyochan.kmpiap.kmpIapInstance - -// Get app transaction (iOS only via KMP) -val appTransaction = kmpIapInstance.getAppTransactionIOS() - -appTransaction?.let { transaction -> - println("Bundle ID: \${transaction.bundleId}") - println("Original version: \${transaction.originalAppVersion}") - println("Environment: \${transaction.environment}") - - // Check if user originally purchased on a different platform (iOS 18.4+) - transaction.originalPlatform?.let { platform -> - println("Originally purchased on: $platform") - } -}`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -// Get app transaction (iOS only) -final appTransaction = await FlutterInappPurchase.instance.getAppTransactionIOS(); - -if (appTransaction != null) { - print('Bundle ID: \${appTransaction.bundleId}'); - print('Original version: \${appTransaction.originalAppVersion}'); - print('Environment: \${appTransaction.environment}'); - - // Check if user originally purchased on a different platform (iOS 18.4+) - if (appTransaction.originalPlatform != null) { - print('Originally purchased on: \${appTransaction.originalPlatform}'); - } -}`} - ), - gdscript: ( - {`# Get app transaction (iOS only) -var app_transaction = await iap.get_app_transaction_ios() - -if app_transaction != null: - print("Bundle ID: %s" % app_transaction.bundle_id) - print("Original version: %s" % app_transaction.original_app_version) - print("Environment: %s" % app_transaction.environment) - - # Check if user originally purchased on a different platform (iOS 18.4+) - if app_transaction.original_platform != "": - print("Originally purchased on: %s" % app_transaction.original_platform)`} - ), - }} - -
    -
    - ); -} - -export default TypesIOS; 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 new file mode 100644 index 000000000..1fe29e472 --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/app-transaction-ios.tsx @@ -0,0 +1,344 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../../components/AnchorLink'; +import CodeBlock from '../../../../components/CodeBlock'; +import LanguageTabs from '../../../../components/LanguageTabs'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function AppTransactionIos() { + useScrollToHash(); + + return ( +
    + +

    AppTransactionIOS

    +
    + + AppTransaction + +

    + Represents the app transaction information returned by{' '} + + getAppTransactionIOS() + + . Contains metadata about the app's purchase and installation. +

    +

    + Native reference:{' '} + + Apple · StoreKit AppTransaction + +

    + + + Fields + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + bundleId + App bundle identifier
    + appVersion + Current app version
    + originalAppVersion + Version when user originally purchased/downloaded
    + originalPurchaseDate + Original purchase timestamp
    + deviceVerification + Device verification data
    + deviceVerificationNonce + Nonce for device verification
    + environment + + Environment: "Sandbox" or "Production" +
    + signedDate + Date when the transaction was signed
    + appId + App ID number
    + appVersionId + App version ID number
    + preorderDate + Preorder date (optional)
    + appTransactionId + App transaction ID (iOS 18.4+)
    + originalPlatform + Original platform (iOS 18.4+)
    + + + Type Definition + + + {{ + typescript: ( + {`interface AppTransaction { + bundleId: string; + appVersion: string; + originalAppVersion: string; + originalPurchaseDate: number; // epoch ms + deviceVerification: string; + deviceVerificationNonce: string; + environment: 'Sandbox' | 'Production'; + signedDate: number; // epoch ms + appId: number; + appVersionId: number; + preorderDate?: number; // epoch ms + // iOS 18.4+ properties + appTransactionId?: string; + originalPlatform?: string; +}`} + ), + swift: ( + {`struct AppTransaction { + let bundleId: String + let appVersion: String + let originalAppVersion: String + let originalPurchaseDate: Date + let deviceVerification: String + let deviceVerificationNonce: String + let environment: String // "Sandbox" | "Production" + let signedDate: Date + let appId: Int + let appVersionId: Int + let preorderDate: Date? + // iOS 18.4+ properties + let appTransactionId: String? + let originalPlatform: String? +}`} + ), + kotlin: ( + {`data class AppTransaction( + val bundleId: String, + val appVersion: String, + val originalAppVersion: String, + val originalPurchaseDate: Long, // epoch ms + val deviceVerification: String, + val deviceVerificationNonce: String, + val environment: String, // "Sandbox" | "Production" + val signedDate: Long, // epoch ms + val appId: Long, + val appVersionId: Long, + val preorderDate: Long? = null, + // iOS 18.4+ properties + val appTransactionId: String? = null, + val originalPlatform: String? = null +)`} + ), + dart: ( + {`class AppTransaction { + final String bundleId; + final String appVersion; + final String originalAppVersion; + final int originalPurchaseDate; // epoch ms + final String deviceVerification; + final String deviceVerificationNonce; + final String environment; // "Sandbox" | "Production" + final int signedDate; // epoch ms + final int appId; + final int appVersionId; + final int? preorderDate; + // iOS 18.4+ properties + final String? appTransactionId; + final String? originalPlatform; + + AppTransaction({ + required this.bundleId, + required this.appVersion, + required this.originalAppVersion, + required this.originalPurchaseDate, + required this.deviceVerification, + required this.deviceVerificationNonce, + required this.environment, + required this.signedDate, + required this.appId, + required this.appVersionId, + this.preorderDate, + this.appTransactionId, + this.originalPlatform, + }); +}`} + ), + gdscript: ( + {`class_name AppTransaction + +var bundle_id: String +var app_version: String +var original_app_version: String +var original_purchase_date: int # epoch ms +var device_verification: String +var device_verification_nonce: String +var environment: String # "Sandbox" | "Production" +var signed_date: int # epoch ms +var app_id: int +var app_version_id: int +var preorder_date: int # optional, epoch ms +# iOS 18.4+ properties +var app_transaction_id: String # optional +var original_platform: String # optional`} + ), + }} + + + + Usage Example + + + {{ + typescript: ( + {`import { getAppTransactionIOS } from 'expo-iap'; + +// Get app transaction (iOS only) +const appTransaction = await getAppTransactionIOS(); + +if (appTransaction) { + console.log('Bundle ID:', appTransaction.bundleId); + console.log('Original version:', appTransaction.originalAppVersion); + console.log('Environment:', appTransaction.environment); + + // Check if user originally purchased on a different platform (iOS 18.4+) + if (appTransaction.originalPlatform) { + console.log('Originally purchased on:', appTransaction.originalPlatform); + } +}`} + ), + swift: ( + {`import OpenIap + +// Get app transaction (iOS only) +let appTransaction = try await OpenIapModule.shared.getAppTransactionIOS() + +if let transaction = appTransaction { + print("Bundle ID: \\(transaction.bundleId)") + print("Original version: \\(transaction.originalAppVersion)") + print("Environment: \\(transaction.environment)") + + // Check if user originally purchased on a different platform (iOS 18.4+) + if let platform = transaction.originalPlatform { + print("Originally purchased on: \\(platform)") + } +}`} + ), + kotlin: ( + {`import io.github.hyochan.kmpiap.kmpIapInstance + +// Get app transaction (iOS only via KMP) +val appTransaction = kmpIapInstance.getAppTransactionIOS() + +appTransaction?.let { transaction -> + println("Bundle ID: \${transaction.bundleId}") + println("Original version: \${transaction.originalAppVersion}") + println("Environment: \${transaction.environment}") + + // Check if user originally purchased on a different platform (iOS 18.4+) + transaction.originalPlatform?.let { platform -> + println("Originally purchased on: $platform") + } +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +// Get app transaction (iOS only) +final appTransaction = await FlutterInappPurchase.instance.getAppTransactionIOS(); + +if (appTransaction != null) { + print('Bundle ID: \${appTransaction.bundleId}'); + print('Original version: \${appTransaction.originalAppVersion}'); + print('Environment: \${appTransaction.environment}'); + + // Check if user originally purchased on a different platform (iOS 18.4+) + if (appTransaction.originalPlatform != null) { + print('Originally purchased on: \${appTransaction.originalPlatform}'); + } +}`} + ), + gdscript: ( + {`# Get app transaction (iOS only) +var app_transaction = await iap.get_app_transaction_ios() + +if app_transaction != null: + print("Bundle ID: %s" % app_transaction.bundle_id) + print("Original version: %s" % app_transaction.original_app_version) + print("Environment: %s" % app_transaction.environment) + + # Check if user originally purchased on a different platform (iOS 18.4+) + if app_transaction.original_platform != "": + print("Originally purchased on: %s" % app_transaction.original_platform)`} + ), + }} + +
    +
    + ); +} + +export default AppTransactionIos; diff --git a/packages/docs/src/pages/docs/types/ios/discount-ios.tsx b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx new file mode 100644 index 000000000..93cfcb373 --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/discount-ios.tsx @@ -0,0 +1,102 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function DiscountIos() { + useScrollToHash(); + + return ( +
    + +

    DiscountIOS

    +
    + + DiscountIOS Deprecated + +

    + Deprecated: Use{' '} + SubscriptionOffer{' '} + instead. +

    +

    Discount info returned as part of product details:

    +

    + Native reference:{' '} + + Apple · SKProductDiscount (StoreKit 1) + +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + identifier + Discount identifier
    + type + Discount type (introductory, promotional)
    + numberOfPeriods + Number of billing periods
    + price + Numeric discount price
    + localizedPrice + Formatted price string with currency symbol
    + priceAmount + Numeric price value (legacy alias)
    + paymentMode + Payment mode (FreeTrial, PayAsYouGo, PayUpFront)
    + subscriptionPeriod + Period duration string
    +
    +
    + ); +} + +export default DiscountIos; diff --git a/packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx b/packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx new file mode 100644 index 000000000..8df79822e --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/discount-offer-ios.tsx @@ -0,0 +1,87 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function DiscountOfferIos() { + useScrollToHash(); + + return ( +
    + +

    DiscountOfferIOS

    +
    + + DiscountOfferIOS Deprecated + +

    + Deprecated: Use{' '} + SubscriptionOffer{' '} + instead. +

    +

    + Used when requesting a purchase with a promotional offer. Generate + signature server-side. +

    +

    + Native reference:{' '} + + Apple · Product.PurchaseOption.promotionalOffer + +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + identifier + Discount identifier from App Store Connect
    + keyIdentifier + Key ID for signature validation
    + nonce + Cryptographic nonce (UUID)
    + signature + Server-generated signature
    + timestamp + Timestamp when signature was generated
    +
    +
    + ); +} + +export default DiscountOfferIos; diff --git a/packages/docs/src/pages/docs/types/ios/payment-mode-ios.tsx b/packages/docs/src/pages/docs/types/ios/payment-mode-ios.tsx new file mode 100644 index 000000000..e9b9f9716 --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/payment-mode-ios.tsx @@ -0,0 +1,65 @@ +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function PaymentModeIos() { + useScrollToHash(); + + return ( +
    + +

    PaymentMode

    +
    + + PaymentMode + +

    Payment mode for offers:

    +

    + Native reference:{' '} + + Apple · Product.SubscriptionOffer.PaymentMode + +

    + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + FreeTrial + Free trial period
    + PayAsYouGo + Pay each period at reduced price
    + PayUpFront + Pay full amount upfront
    +
    +
    + ); +} + +export default PaymentModeIos; diff --git a/packages/docs/src/pages/docs/types/ios/renewal-info-ios.tsx b/packages/docs/src/pages/docs/types/ios/renewal-info-ios.tsx new file mode 100644 index 000000000..abd2ea219 --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/renewal-info-ios.tsx @@ -0,0 +1,135 @@ +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function RenewalInfoIOS() { + useScrollToHash(); + + return ( +
    + +

    + iOS{' '} + RenewalInfoIOS +

    +

    + Subscription renewal details exposed by StoreKit 2's{' '} + + Product.SubscriptionInfo.RenewalInfo + + . Carries auto-renewal intent, billing-retry state, price-increase + responses, and the JWS payload for server-side verification. +

    + +
    + + RenewalInfoIOS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + willAutoRenew + Whether the subscription will automatically renew.
    + autoRenewPreference + + Product ID the subscription will renew to (may differ if an + upgrade or downgrade is pending). +
    + expirationReason + + Why the subscription expired: "VOLUNTARY",{' '} + "BILLING_ERROR",{' '} + "DID_NOT_AGREE_TO_PRICE_INCREASE",{' '} + "PRODUCT_NOT_AVAILABLE", "UNKNOWN". +
    + gracePeriodExpirationDate + Grace-period end timestamp (epoch ms).
    + isInBillingRetry + True if Apple is retrying after a billing failure.
    + pendingUpgradeProductId + Product ID for the pending upgrade/downgrade.
    + priceIncreaseStatus + + Price-increase response: "AGREED",{' '} + "PENDING", or null. +
    + renewalDate + Expected renewal timestamp (epoch ms).
    + renewalOfferId + Offer ID applied to the next renewal.
    + renewalOfferType + + Offer type: "PROMOTIONAL",{' '} + "SUBSCRIPTION_OFFER_CODE", "WIN_BACK". +
    + jsonRepresentation + + Raw JWS representation of the StoreKit renewal info — useful for + server-side validation. +
    +
    +
    + ); +} + +export default RenewalInfoIOS; diff --git a/packages/docs/src/pages/docs/types/ios/subscription-period-ios.tsx b/packages/docs/src/pages/docs/types/ios/subscription-period-ios.tsx new file mode 100644 index 000000000..5997494de --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/subscription-period-ios.tsx @@ -0,0 +1,54 @@ +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function SubscriptionPeriodIos() { + useScrollToHash(); + + return ( +
    + +

    SubscriptionPeriodIOS

    +
    + + SubscriptionPeriodIOS + +

    Subscription period units:

    +

    + Native reference:{' '} + + Apple · Product.SubscriptionPeriod + +

    + + + + + + + + + + + + + +
    NameSummary
    + Day, Week, Month,{' '} + Year + Available subscription period units
    +
    +
    + ); +} + +export default SubscriptionPeriodIos; diff --git a/packages/docs/src/pages/docs/types/ios/subscription-status-ios.tsx b/packages/docs/src/pages/docs/types/ios/subscription-status-ios.tsx new file mode 100644 index 000000000..f17880d20 --- /dev/null +++ b/packages/docs/src/pages/docs/types/ios/subscription-status-ios.tsx @@ -0,0 +1,170 @@ +import AnchorLink from '../../../../components/AnchorLink'; +import SEO from '../../../../components/SEO'; +import { useScrollToHash } from '../../../../hooks/useScrollToHash'; + +function SubscriptionStatusIos() { + useScrollToHash(); + + return ( +
    + +

    SubscriptionStatusIOS

    +
    + + SubscriptionStatusIOS + +

    + Subscription status from StoreKit 2. Use{' '} + subscriptionStatusIOS(sku) to get detailed subscription + state. +

    +

    + Native reference:{' '} + + Apple · Product.SubscriptionInfo.RenewalState + +

    + + + + + + + + + + + + + + + + + + +
    NameSummary
    + state + Current renewal state (see values below)
    + renewalInfo + + Renewal details. Contains: willAutoRenew,{' '} + autoRenewPreference +
    + + + Subscription State Values + +

    + The state field indicates the current subscription + status: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    StateDescriptionUser Access
    + subscribed + Active subscriptionGrant access
    + expired + Subscription has expiredDeny access
    + revoked + Refunded by AppleDeny access
    + inGracePeriod + Billing failed but grace period activeGrant access (temporary)
    + inBillingRetryPeriod + Billing retry in progressConsider granting access
    + + + iOS Expiration Reasons + +

    + When willAutoRenew is false, the{' '} + expirationReason field in renewalInfo{' '} + indicates why: +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ReasonDescription
    + VOLUNTARY + User cancelled the subscription
    + BILLING_ERROR + Payment failed (card declined, etc.)
    + DID_NOT_AGREE_TO_PRICE_INCREASE + User declined a price increase
    + PRODUCT_NOT_AVAILABLE + Product no longer available for purchase
    + UNKNOWN + Unknown reason
    +
    +
    + ); +} + +export default SubscriptionStatusIos; diff --git a/packages/docs/src/pages/docs/types/offer.tsx b/packages/docs/src/pages/docs/types/offer.tsx deleted file mode 100644 index ed51e2223..000000000 --- a/packages/docs/src/pages/docs/types/offer.tsx +++ /dev/null @@ -1,1239 +0,0 @@ -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; - -function TypesOffer() { - useScrollToHash(); - - return ( -
    - -

    Discount & Subscription Offer Types

    -

    - Standardized cross-platform types for handling discounts and - subscription offers. These types provide a unified interface while - preserving platform-specific functionality through suffixed fields. -

    - - -
      -
    • - - DiscountOffer - {' '} - - One-time product discounts (Android 7.0+) -
    • -
    • - - SubscriptionOffer - {' '} - - Subscription discounts (iOS & Android) -
    • -
    • - Platform-specific fields use IOS or{' '} - Android suffix -
    • -
    • - Deprecated:{' '} - - DiscountIOS, DiscountOfferIOS, SubscriptionOfferIOS, - ProductAndroidOneTimePurchaseOfferDetail, - ProductSubscriptionAndroidOfferDetails - -
    • -
    -
    - -
    -

    - Migration Note: The legacy platform-specific types - are now deprecated. Use these standardized types for new - implementations and migrate existing code when convenient. -

    -
    - -
    - - DiscountOffer - -

    - Standardized type for one-time product discount offers. Currently - supported on Android (Google Play Billing Library 7.0+). iOS does not - support one-time purchase discounts. -

    - - - Common Fields - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    FieldTypeDescription
    - id - - ID - Unique identifier for the offer
    - displayPrice - - String! - Formatted display price (e.g., "$4.99")
    - price - - Float! - Numeric price value
    - currency - - String! - Currency code (ISO 4217, e.g., "USD")
    - type - - DiscountOfferType! - - Type of offer: Introductory,{' '} - Promotional, WinBack (iOS 18+), or{' '} - OneTime -
    - - - Android-Specific Fields - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    FieldTypeDescription
    - offerTokenAndroid - - String - - Required for purchase. Pass to - requestPurchase() -
    - offerTagsAndroid - - [String!] - Tags associated with this offer
    - fullPriceMicrosAndroid - - String - Original price in micro-units (divide by 1,000,000)
    - percentageDiscountAndroid - - Int - Percentage discount (e.g., 33 for 33% off)
    - discountAmountMicrosAndroid - - String - Fixed discount amount in micro-units
    - formattedDiscountAmountAndroid - - String - Formatted discount amount (e.g., "$5.00 OFF")
    - validTimeWindowAndroid - - ValidTimeWindowAndroid - Time window for limited-time offers
    - limitedQuantityInfoAndroid - - LimitedQuantityInfoAndroid - Quantity limits for the offer
    - preorderDetailsAndroid - - PreorderDetailsAndroid - Pre-order details (Billing Library 8.1.0+)
    - rentalDetailsAndroid - - RentalDetailsAndroid - Rental offer details
    - purchaseOptionIdAndroid - - String - - Purchase option ID for identifying which purchase option was - selected (7.0+) -
    - - - Type Definition - - - {{ - typescript: ( - {`interface DiscountOffer { - // Common fields - id: string | null; - displayPrice: string; - price: number; - currency: string; - type: DiscountOfferType; - - // Android-specific fields - offerTokenAndroid?: string; - offerTagsAndroid?: string[]; - fullPriceMicrosAndroid?: string; - percentageDiscountAndroid?: number; - discountAmountMicrosAndroid?: string; - formattedDiscountAmountAndroid?: string; - validTimeWindowAndroid?: ValidTimeWindowAndroid; - limitedQuantityInfoAndroid?: LimitedQuantityInfoAndroid; - preorderDetailsAndroid?: PreorderDetailsAndroid; - rentalDetailsAndroid?: RentalDetailsAndroid; - purchaseOptionIdAndroid?: string; -} - -enum DiscountOfferType { - Introductory = 'Introductory', - Promotional = 'Promotional', - WinBack = 'WinBack', // iOS 18+ - OneTime = 'OneTime', -}`} - ), - swift: ( - {`struct DiscountOffer: Codable { - // Common fields - let id: String? - let displayPrice: String - let price: Double - let currency: String - let type: DiscountOfferType - - // Android-specific fields - let offerTokenAndroid: String? - let offerTagsAndroid: [String]? - let fullPriceMicrosAndroid: String? - let percentageDiscountAndroid: Int? - let discountAmountMicrosAndroid: String? - let formattedDiscountAmountAndroid: String? - let validTimeWindowAndroid: ValidTimeWindowAndroid? - let limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? - let preorderDetailsAndroid: PreorderDetailsAndroid? - let rentalDetailsAndroid: RentalDetailsAndroid? - let purchaseOptionIdAndroid: String? -} - -enum DiscountOfferType: String, Codable { - case introductory = "Introductory" - case promotional = "Promotional" - case winBack = "WinBack" // iOS 18+ - case oneTime = "OneTime" -}`} - ), - kotlin: ( - {`data class DiscountOffer( - // Common fields - val id: String?, - val displayPrice: String, - val price: Double, - val currency: String, - val type: DiscountOfferType, - - // Android-specific fields - val offerTokenAndroid: String? = null, - val offerTagsAndroid: List? = null, - val fullPriceMicrosAndroid: String? = null, - val percentageDiscountAndroid: Int? = null, - val discountAmountMicrosAndroid: String? = null, - val formattedDiscountAmountAndroid: String? = null, - val validTimeWindowAndroid: ValidTimeWindowAndroid? = null, - val limitedQuantityInfoAndroid: LimitedQuantityInfoAndroid? = null, - val preorderDetailsAndroid: PreorderDetailsAndroid? = null, - val rentalDetailsAndroid: RentalDetailsAndroid? = null, - val purchaseOptionIdAndroid: String? = null -) - -enum class DiscountOfferType { - Introductory, - Promotional, - WinBack, // iOS 18+ - OneTime -}`} - ), - dart: ( - {`class DiscountOffer { - // Common fields - final String? id; - final String displayPrice; - final double price; - final String currency; - final DiscountOfferType type; - - // Android-specific fields - final String? offerTokenAndroid; - final List? offerTagsAndroid; - final String? fullPriceMicrosAndroid; - final int? percentageDiscountAndroid; - final String? discountAmountMicrosAndroid; - final String? formattedDiscountAmountAndroid; - final ValidTimeWindowAndroid? validTimeWindowAndroid; - final LimitedQuantityInfoAndroid? limitedQuantityInfoAndroid; - final PreorderDetailsAndroid? preorderDetailsAndroid; - final RentalDetailsAndroid? rentalDetailsAndroid; - final String? purchaseOptionIdAndroid; - - DiscountOffer({ - this.id, - required this.displayPrice, - required this.price, - required this.currency, - required this.type, - this.offerTokenAndroid, - this.offerTagsAndroid, - this.fullPriceMicrosAndroid, - this.percentageDiscountAndroid, - this.discountAmountMicrosAndroid, - this.formattedDiscountAmountAndroid, - this.validTimeWindowAndroid, - this.limitedQuantityInfoAndroid, - this.preorderDetailsAndroid, - this.rentalDetailsAndroid, - this.purchaseOptionIdAndroid, - }); -} - -enum DiscountOfferType { - introductory, - promotional, - winBack, // iOS 18+ - oneTime, -}`} - ), - gdscript: ( - {`class_name DiscountOffer - -# Common fields -var id: String -var display_price: String -var price: float -var currency: String -var type: DiscountOfferType - -# Android-specific fields -var offer_token_android: String -var offer_tags_android: Array[String] -var full_price_micros_android: String -var percentage_discount_android: int -var discount_amount_micros_android: String -var formatted_discount_amount_android: String -var valid_time_window_android: ValidTimeWindowAndroid -var limited_quantity_info_android: LimitedQuantityInfoAndroid -var preorder_details_android: PreorderDetailsAndroid -var rental_details_android: RentalDetailsAndroid -var purchase_option_id_android: String - -enum DiscountOfferType { - INTRODUCTORY, - PROMOTIONAL, - WIN_BACK, # iOS 18+ - ONE_TIME -}`} - ), - }} - -
    - -
    - - SubscriptionOffer - -

    - Standardized type for subscription promotional offers. Supported on - both iOS (introductory and promotional offers) and Android (offer - tokens with pricing phases). -

    - - - Common Fields - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    FieldTypeDescription
    - id - - ID! - Unique identifier for the offer
    - displayPrice - - String! - Formatted display price (e.g., "$9.99/month")
    - price - - Float! - Numeric price value
    - currency - - String - Currency code (ISO 4217)
    - type - - DiscountOfferType! - - Introductory, Promotional, or{' '} - WinBack (iOS 18+) -
    - period - - SubscriptionPeriod - Subscription period (unit + value)
    - periodCount - - Int - Number of periods the offer applies
    - paymentMode - - PaymentMode - FreeTrial, PayAsYouGo, or PayUpFront
    - - - iOS-Specific Fields - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    FieldTypeDescription
    - keyIdentifierIOS - - String - Key ID for server-side signature validation
    - nonceIOS - - String - Cryptographic nonce (UUID) for signature
    - signatureIOS - - String - Server-generated signature for validation
    - timestampIOS - - Float - Timestamp when signature was generated
    - numberOfPeriodsIOS - - Int - Number of billing periods for this discount
    - localizedPriceIOS - - String - Localized price string
    - - - Android-Specific Fields - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    FieldTypeDescription
    - basePlanIdAndroid - - String - Base plan identifier
    - offerTokenAndroid - - String - - Required for purchase. Pass to - requestPurchase() -
    - offerTagsAndroid - - [String!] - Tags associated with this offer
    - pricingPhasesAndroid - - PricingPhasesAndroid - Pricing phases (trial, intro, regular)
    - installmentPlanDetailsAndroid - - InstallmentPlanDetailsAndroid - - Installment plan details for subscription commitments (7.0+) -
    - - - Type Definition - - - {{ - typescript: ( - {`interface SubscriptionOffer { - // Common fields - id: string; - displayPrice: string; - price: number; - currency?: string; - type: DiscountOfferType; - period?: SubscriptionPeriod; - periodCount?: number; - paymentMode?: PaymentMode; - - // iOS-specific fields - keyIdentifierIOS?: string; - nonceIOS?: string; - signatureIOS?: string; - timestampIOS?: number; - numberOfPeriodsIOS?: number; - localizedPriceIOS?: string; - - // Android-specific fields - basePlanIdAndroid?: string; - offerTokenAndroid?: string; - offerTagsAndroid?: string[]; - pricingPhasesAndroid?: PricingPhasesAndroid; - installmentPlanDetailsAndroid?: InstallmentPlanDetailsAndroid; -} - -interface InstallmentPlanDetailsAndroid { - commitmentPaymentsCount: number; - subsequentCommitmentPaymentsCount: number; -} - -interface SubscriptionPeriod { - unit: SubscriptionPeriodUnit; - value: number; -} - -enum SubscriptionPeriodUnit { - Day = 'Day', - Week = 'Week', - Month = 'Month', - Year = 'Year', - Unknown = 'Unknown', -} - -enum PaymentMode { - FreeTrial = 'FreeTrial', - PayAsYouGo = 'PayAsYouGo', - PayUpFront = 'PayUpFront', - Unknown = 'Unknown', -}`} - ), - swift: ( - {`struct SubscriptionOffer: Codable { - // Common fields - let id: String - let displayPrice: String - let price: Double - let currency: String? - let type: DiscountOfferType - let period: SubscriptionPeriod? - let periodCount: Int? - let paymentMode: PaymentMode? - - // iOS-specific fields - let keyIdentifierIOS: String? - let nonceIOS: String? - let signatureIOS: String? - let timestampIOS: Double? - let numberOfPeriodsIOS: Int? - let localizedPriceIOS: String? - - // Android-specific fields - let basePlanIdAndroid: String? - let offerTokenAndroid: String? - let offerTagsAndroid: [String]? - let pricingPhasesAndroid: PricingPhasesAndroid? - let installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? -} - -struct InstallmentPlanDetailsAndroid: Codable { - let commitmentPaymentsCount: Int - let subsequentCommitmentPaymentsCount: Int -} - -struct SubscriptionPeriod: Codable { - let unit: SubscriptionPeriodUnit - let value: Int -} - -enum SubscriptionPeriodUnit: String, Codable { - case day = "Day" - case week = "Week" - case month = "Month" - case year = "Year" - case unknown = "Unknown" -} - -enum PaymentMode: String, Codable { - case freeTrial = "FreeTrial" - case payAsYouGo = "PayAsYouGo" - case payUpFront = "PayUpFront" - case unknown = "Unknown" -}`} - ), - kotlin: ( - {`data class SubscriptionOffer( - // Common fields - val id: String, - val displayPrice: String, - val price: Double, - val currency: String? = null, - val type: DiscountOfferType, - val period: SubscriptionPeriod? = null, - val periodCount: Int? = null, - val paymentMode: PaymentMode? = null, - - // iOS-specific fields - val keyIdentifierIOS: String? = null, - val nonceIOS: String? = null, - val signatureIOS: String? = null, - val timestampIOS: Double? = null, - val numberOfPeriodsIOS: Int? = null, - val localizedPriceIOS: String? = null, - - // Android-specific fields - val basePlanIdAndroid: String? = null, - val offerTokenAndroid: String? = null, - val offerTagsAndroid: List? = null, - val pricingPhasesAndroid: PricingPhasesAndroid? = null, - val installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = null -) - -data class InstallmentPlanDetailsAndroid( - val commitmentPaymentsCount: Int, - val subsequentCommitmentPaymentsCount: Int -) - -data class SubscriptionPeriod( - val unit: SubscriptionPeriodUnit, - val value: Int -) - -enum class SubscriptionPeriodUnit { - Day, Week, Month, Year, Unknown -} - -enum class PaymentMode { - FreeTrial, PayAsYouGo, PayUpFront, Unknown -}`} - ), - dart: ( - {`class SubscriptionOffer { - // Common fields - final String id; - final String displayPrice; - final double price; - final String? currency; - final DiscountOfferType type; - final SubscriptionPeriod? period; - final int? periodCount; - final PaymentMode? paymentMode; - - // iOS-specific fields - final String? keyIdentifierIOS; - final String? nonceIOS; - final String? signatureIOS; - final double? timestampIOS; - final int? numberOfPeriodsIOS; - final String? localizedPriceIOS; - - // Android-specific fields - final String? basePlanIdAndroid; - final String? offerTokenAndroid; - final List? offerTagsAndroid; - final PricingPhasesAndroid? pricingPhasesAndroid; - final InstallmentPlanDetailsAndroid? installmentPlanDetailsAndroid; - - SubscriptionOffer({ - required this.id, - required this.displayPrice, - required this.price, - this.currency, - required this.type, - this.period, - this.periodCount, - this.paymentMode, - this.keyIdentifierIOS, - this.nonceIOS, - this.signatureIOS, - this.timestampIOS, - this.numberOfPeriodsIOS, - this.localizedPriceIOS, - this.basePlanIdAndroid, - this.offerTokenAndroid, - this.offerTagsAndroid, - this.pricingPhasesAndroid, - this.installmentPlanDetailsAndroid, - }); -} - -class InstallmentPlanDetailsAndroid { - final int commitmentPaymentsCount; - final int subsequentCommitmentPaymentsCount; - - InstallmentPlanDetailsAndroid({ - required this.commitmentPaymentsCount, - required this.subsequentCommitmentPaymentsCount, - }); -} - -class SubscriptionPeriod { - final SubscriptionPeriodUnit unit; - final int value; - - SubscriptionPeriod({required this.unit, required this.value}); -} - -enum SubscriptionPeriodUnit { day, week, month, year, unknown } - -enum PaymentMode { freeTrial, payAsYouGo, payUpFront, unknown }`} - ), - gdscript: ( - {`class_name SubscriptionOffer - -# Common fields -var id: String -var display_price: String -var price: float -var currency: String -var type: DiscountOfferType -var period: SubscriptionPeriod -var period_count: int -var payment_mode: PaymentMode - -# iOS-specific fields -var key_identifier_ios: String -var nonce_ios: String -var signature_ios: String -var timestamp_ios: float -var number_of_periods_ios: int -var localized_price_ios: String - -# Android-specific fields -var base_plan_id_android: String -var offer_token_android: String -var offer_tags_android: Array[String] -var pricing_phases_android: PricingPhasesAndroid -var installment_plan_details_android: InstallmentPlanDetailsAndroid - -class InstallmentPlanDetailsAndroid: - var commitment_payments_count: int - var subsequent_commitment_payments_count: int - -class SubscriptionPeriod: - var unit: SubscriptionPeriodUnit - var value: int - -enum SubscriptionPeriodUnit { DAY, WEEK, MONTH, YEAR, UNKNOWN } -enum PaymentMode { FREE_TRIAL, PAY_AS_YOU_GO, PAY_UP_FRONT, UNKNOWN }`} - ), - }} - -
    - -
    - - Usage Example - -

    - Access standardized offers from products and use platform-specific - fields when needed: -

    - - - {{ - typescript: ( - {`import { fetchProducts, requestPurchase, Product } from 'expo-iap'; - -const products = await fetchProducts({ - skus: ['premium_feature', 'premium_subscription'], -}); - -for (const product of products) { - // Access standardized discount offers (one-time products) - const discountOffers = product.discountOffers; - if (discountOffers && discountOffers.length > 0) { - const offer = discountOffers[0]; - console.log('Discount:', offer.displayPrice); - console.log('Original:', offer.fullPriceMicrosAndroid); - console.log('Percentage off:', offer.percentageDiscountAndroid); - } - - // Access standardized subscription offers - const subscriptionOffers = product.subscriptionOffers; - if (subscriptionOffers && subscriptionOffers.length > 0) { - const offer = subscriptionOffers[0]; - console.log('Subscription offer:', offer.displayPrice); - console.log('Period:', offer.period?.unit, offer.period?.value); - console.log('Payment mode:', offer.paymentMode); - - // Platform-specific: Android needs offerToken - if (offer.offerTokenAndroid) { - await requestPurchase({ - request: { - google: { - skus: [product.id], - subscriptionOffers: [{ - sku: product.id, - offerToken: offer.offerTokenAndroid, - }], - }, - }, - type: 'subs', - }); - } - - // Platform-specific: iOS needs server-side signature for promotional offers - if (offer.signatureIOS) { - await requestPurchase({ - request: { - apple: { - sku: product.id, - withOffer: { - identifier: offer.id, - keyIdentifier: offer.keyIdentifierIOS!, - nonce: offer.nonceIOS!, - signature: offer.signatureIOS, - timestamp: offer.timestampIOS!, - }, - }, - }, - type: 'subs', - }); - } - } -}`} - ), - kotlin: ( - {`import dev.hyo.openiap.OpenIapModule -import dev.hyo.openiap.types.* - -val products = openIapModule.fetchProducts( - skus = listOf("premium_feature", "premium_subscription"), - type = ProductQueryType.All -) - -products.forEach { product -> - // Access standardized discount offers (one-time products) - product.discountOffers?.forEach { offer -> - println("Discount: \${offer.displayPrice}") - println("Original: \${offer.fullPriceMicrosAndroid}") - println("Percentage off: \${offer.percentageDiscountAndroid}") - } - - // Access standardized subscription offers - product.subscriptionOffers?.forEach { offer -> - println("Subscription offer: \${offer.displayPrice}") - println("Period: \${offer.period?.unit} \${offer.period?.value}") - println("Payment mode: \${offer.paymentMode}") - - // Use offerToken for Android purchases - offer.offerTokenAndroid?.let { token -> - openIapModule.requestPurchase( - sku = product.id, - subscriptionOffers = listOf( - SubscriptionOfferAndroid( - sku = product.id, - offerToken = token - ) - ) - ) - } - } -}`} - ), - swift: ( - {`import OpenIap - -let products = try await OpenIapModule.shared.fetchProducts( - skus: ["premium_feature", "premium_subscription"] -) - -for product in products { - // Access standardized subscription offers - if let offers = product.subscriptionOffers { - for offer in offers { - print("Subscription offer: \\(offer.displayPrice)") - if let period = offer.period { - print("Period: \\(period.unit) \\(period.value)") - } - print("Payment mode: \\(offer.paymentMode ?? .unknown)") - - // iOS promotional offers require server-side signature - if let signature = offer.signatureIOS, - let keyId = offer.keyIdentifierIOS, - let nonce = offer.nonceIOS, - let timestamp = offer.timestampIOS { - try await OpenIapModule.shared.requestPurchase( - sku: product.id, - withOffer: DiscountOfferInputIOS( - identifier: offer.id, - keyIdentifier: keyId, - nonce: nonce, - signature: signature, - timestamp: timestamp - ) - ) - } - } - } -}`} - ), - gdscript: ( - {`var request = ProductRequest.new() -request.skus = ["premium_feature", "premium_subscription"] -request.type = ProductQueryType.ALL -var products = await iap.fetch_products(request) - -for product in products: - # Access standardized discount offers (one-time products) - if product.discount_offers: - for offer in product.discount_offers: - print("Discount: %s" % offer.display_price) - print("Original: %s" % offer.full_price_micros_android) - print("Percentage off: %d" % offer.percentage_discount_android) - - # Access standardized subscription offers - if product.subscription_offers: - for offer in product.subscription_offers: - print("Subscription offer: %s" % offer.display_price) - if offer.period: - print("Period: %s %d" % [offer.period.unit, offer.period.value]) - print("Payment mode: %s" % offer.payment_mode) - - # Use offerToken for Android purchases - if offer.offer_token_android: - var props = RequestPurchaseProps.new() - props.request = RequestSubscriptionPropsByPlatforms.new() - props.request.google = RequestSubscriptionAndroidProps.new() - props.request.google.skus = [product.id] - props.request.google.subscription_offers = [{ - "sku": product.id, - "offerToken": offer.offer_token_android - }] - props.type = ProductQueryType.SUBS - await iap.request_purchase(props)`} - ), - }} - -
    - -
    - - Migration Guide - -

    - Migrate from deprecated platform-specific types to the new - standardized types: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    Deprecated TypeNew TypeNotes
    - DiscountIOS - - SubscriptionOffer - Use iOS-suffixed fields for platform-specific data
    - DiscountOfferIOS - - SubscriptionOffer - - Signature fields: keyIdentifierIOS,{' '} - nonceIOS, etc. -
    - SubscriptionOfferIOS - - SubscriptionOffer - - Period info in common period field -
    - ProductAndroidOneTimePurchaseOfferDetail - - DiscountOffer - Use Android-suffixed fields
    - ProductSubscriptionAndroidOfferDetails - - SubscriptionOffer - - pricingPhasesAndroid for detailed phases -
    - subscriptionInfoIOS - - subscriptionOffers - Field on Product types
    - oneTimePurchaseOfferDetailsAndroid - - discountOffers - Field on Product types
    - subscriptionOfferDetailsAndroid - - subscriptionOffers - Field on Product types
    - -
    -

    - Backward Compatibility: The deprecated types and - fields are still available but will be removed in a future major - version. Plan your migration accordingly. -

    -
    -
    -
    - ); -} - -export default TypesOffer; diff --git a/packages/docs/src/pages/docs/types/product-request.tsx b/packages/docs/src/pages/docs/types/product-request.tsx new file mode 100644 index 000000000..cf393688b --- /dev/null +++ b/packages/docs/src/pages/docs/types/product-request.tsx @@ -0,0 +1,176 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function ProductRequest() { + useScrollToHash(); + + return ( +
    + +

    ProductRequest

    +
    + + ProductRequest + +

    + Parameters for fetching products from the store via{' '} + + fetchProducts() + + . +

    +

    + Native references:{' '} + + Apple · Product.products(for:) + + {' · '} + + Google · QueryProductDetailsParams + +

    + + + Fields + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + skus + Array of product identifiers to fetch
    + type + + Product type filter (optional): "in-app" (default),{' '} + "subs", or "all" +
    + + + Usage Example + + + {{ + typescript: ( + {`// Fetch in-app purchases (default) +const inappProducts = await fetchProducts({ skus: ["product1", "product2"] }); + +// Fetch only subscriptions +const subscriptions = await fetchProducts({ + skus: ["sub1", "sub2"], + type: "subs" +}); + +// Fetch all products (both in-app and subscriptions) +const allProducts = await fetchProducts({ + skus: ["product1", "sub1"], + type: "all" +});`} + ), + swift: ( + {`// Fetch in-app purchases (default) +let inappProducts = try await OpenIapModule.shared.fetchProducts( + ProductRequest(skus: ["product1", "product2"]) +) + +// Fetch only subscriptions +let subscriptions = try await OpenIapModule.shared.fetchProducts( + ProductRequest(skus: ["sub1", "sub2"], type: .subs) +) + +// Fetch all products (both in-app and subscriptions) +let allProducts = try await OpenIapModule.shared.fetchProducts( + ProductRequest(skus: ["product1", "sub1"], type: .all) +)`} + ), + kotlin: ( + {`// Fetch in-app purchases (default) +val inappProducts = openIapStore.fetchProducts( + ProductRequest(skus = listOf("product1", "product2")) +) + +// Fetch only subscriptions +val subscriptions = openIapStore.fetchProducts( + ProductRequest(skus = listOf("sub1", "sub2"), type = ProductQueryType.Subs) +) + +// Fetch all products (both in-app and subscriptions) +val allProducts = openIapStore.fetchProducts( + ProductRequest(skus = listOf("product1", "sub1"), type = ProductQueryType.All) +)`} + ), + dart: ( + {`// Fetch in-app purchases (default) +final inappProducts = await FlutterInappPurchase.instance.fetchProducts( + skus: ['product1', 'product2'], +); + +// Fetch only subscriptions +final subscriptions = await FlutterInappPurchase.instance.fetchProducts( + skus: ['sub1', 'sub2'], + type: ProductQueryType.subs, +); + +// Fetch all products (both in-app and subscriptions) +final allProducts = await FlutterInappPurchase.instance.fetchProducts( + skus: ['product1', 'sub1'], + type: ProductQueryType.all, +);`} + ), + gdscript: ( + {`# Fetch in-app purchases (default) +var request = ProductRequest.new() +request.skus = ["product1", "product2"] +var inapp_products = await iap.fetch_products(request) + +# Fetch only subscriptions +var subs_request = ProductRequest.new() +subs_request.skus = ["sub1", "sub2"] +subs_request.type = ProductQueryType.SUBS +var subscriptions = await iap.fetch_products(subs_request) + +# Fetch all products (both in-app and subscriptions) +var all_request = ProductRequest.new() +all_request.skus = ["product1", "sub1"] +all_request.type = ProductQueryType.ALL +var all_products = await iap.fetch_products(all_request)`} + ), + }} + +
    +
    + ); +} + +export default ProductRequest; diff --git a/packages/docs/src/pages/docs/types/product.tsx b/packages/docs/src/pages/docs/types/product.tsx index e1e6a535b..24f355907 100644 --- a/packages/docs/src/pages/docs/types/product.tsx +++ b/packages/docs/src/pages/docs/types/product.tsx @@ -1,63 +1,54 @@ +import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; import PlatformTabs from '../../../components/PlatformTabs'; import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; -function TypesProduct() { +function Product() { useScrollToHash(); return (
    -

    Product Types

    -

    - Type definitions for products available in the store, including - subscriptions and platform-specific fields. -

    - - - - - +

    Product

    Product

    Represents a product available for purchase in the store. The type is - a union of ProductIOS and ProductAndroid, - discriminated by the platform field. + a union of{' '} + + ProductIOS + {' '} + and{' '} + + ProductAndroid + + , discriminated by the platform field. +

    +

    + Native references:{' '} + + Apple · StoreKit Product + + {' · '} + + Google · ProductDetails +

    @@ -103,6 +94,12 @@ function TypesProduct() { subscriptions + + + displayName + + Display-friendly product name (optional) + displayPrice @@ -123,6 +120,12 @@ function TypesProduct() { Numeric price value (e.g., 9.99) + + + debugDescription + + Debug-friendly description (optional) + store @@ -134,15 +137,10 @@ function TypesProduct() { - platform{' '} - - (deprecated) - + platform - Use store instead + Deprecated. Use store instead. @@ -192,16 +190,38 @@ function TypesProduct() { - subscriptionInfoIOS + + subscriptionInfoIOS + - Subscription metadata (only for subscriptions). - Contains: subscriptionGroupId,{' '} + Deprecated. Use{' '} + subscriptionOffers instead. Subscription + metadata (only for subscriptions). Contains:{' '} + subscriptionGroupId,{' '} subscriptionPeriod (unit and value),{' '} introductoryOffer,{' '} promotionalOffers + + + subscriptionOffers + + + Cross-platform array of{' '} + + SubscriptionOffer + {' '} + — unified across iOS/Android. + + + + + jsonRepresentationIOS + + Raw StoreKit 2 JWS payload as a JSON string. + @@ -241,7 +261,7 @@ function TypesProduct() { limitedQuantityInfo,{' '} preorderDetailsAndroid,{' '} rentalDetailsAndroid. See{' '} - Discounts. + Discounts. Requires{' '} - - - - ), - }} - -
    - -
    - - SubscriptionProduct - -

    - Represents a subscription product available for purchase. Extends the - base Product type with subscription-specific fields like pricing - phases, introductory offers, and billing periods. -

    - - - Common Fields - -

    - In addition to all Product common fields - , subscription products include: -

    - - - - - - - - - - - - - -
    NameSummary
    - type - - Always "subs" for subscription products -
    - - - Platform-Specific Fields - - - {{ - ios: ( - <> - - SubscriptionProductIOS - -

    Additional fields available on iOS subscriptions:

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - discountsIOS - - Array of available discounts. Each contains:{' '} - identifier, type,{' '} - numberOfPeriods, price,{' '} - localizedPrice, paymentMode,{' '} - subscriptionPeriod -
    - introductoryPriceIOS - Formatted introductory price (e.g., "$0.99")
    - introductoryPriceAsAmountIOS - Numeric introductory price value
    - introductoryPricePaymentModeIOS - - Payment mode for intro offer (FreeTrial, PayAsYouGo, - PayUpFront) -
    - introductoryPriceNumberOfPeriodsIOS - Number of periods for intro pricing
    - introductoryPriceSubscriptionPeriodIOS + discountOffers - Period unit for intro pricing (Day, Week, Month, Year) + Cross-platform array of{' '} + + DiscountOffer + {' '} + — unified discount metadata.
    - subscriptionPeriodNumberIOS + subscriptionOffers Number of units in a subscription period
    - subscriptionPeriodUnitIOS - Period unit (Day, Week, Month, Year)
    - - ), - android: ( - <> - - SubscriptionProductAndroid - -

    Additional fields available on Android subscriptions:

    - - - - - - - - - - - @@ -438,150 +331,8 @@ function TypesProduct() { }} - -
    - - Unified Platform Types - -

    - These types combine platform-specific types with a store{' '} - discriminator for type-safe handling across Apple, Google, and Horizon - stores. -

    - - - Store Discriminators - -

    - Each unified type includes a store field that identifies - the source store: -

    -
    NameSummary
    - subscriptionOfferDetailsAndroid - - Array of subscription offers. Each contains:{' '} - basePlanId, offerId,{' '} - offerToken, pricingPhases,{' '} - offerTags + Cross-platform array of{' '} + + SubscriptionOffer + {' '} + — unified across iOS/Android.
    - - - - - - - - - - - - - - - - - - - - - - - - -
    ValueSummary
    - "apple" - Apple App Store (iOS/macOS)
    - "google" - Google Play Store (Android)
    - "horizon" - Meta Horizon Store (Quest)
    - "unknown" - Unknown store (default)
    -
    -

    - Note: The platform field is - deprecated. Use store instead. -

    -
    - - - Union Types - -

    The SDK provides these unified types for cross-platform code:

    - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - Product - - Union of ProductIOS and ProductAndroid -
    - SubscriptionProduct - - Union of SubscriptionProductIOS and{' '} - SubscriptionProductAndroid -
    - Purchase - - Union of PurchaseIOS and{' '} - PurchaseAndroid -
    -

    - Use the platform field to narrow the type and access - platform-specific fields safely. -

    -
    - -
    - - Storefront - -

    - Represents the user's App Store or Play Store region, returned by{' '} - getStorefront(). -

    - - - - - - - - - - - - - -
    NameSummary
    - StorefrontCode - ISO 3166-1 alpha-2 country code (string)
    -

    - Example values: "US", "KR",{' '} - "JP". May return an empty string when the storefront - cannot be determined. -

    -
    -

    - iOS sources the value from the active StoreKit storefront. Android - queries Google Play Billing configuration and returns the same - country code string when available. -

    -
    -
    ); } -export default TypesProduct; +export default Product; diff --git a/packages/docs/src/pages/docs/types/purchase.tsx b/packages/docs/src/pages/docs/types/purchase.tsx index 17074155f..bab1f7533 100644 --- a/packages/docs/src/pages/docs/types/purchase.tsx +++ b/packages/docs/src/pages/docs/types/purchase.tsx @@ -1,70 +1,61 @@ +import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; import PlatformTabs from '../../../components/PlatformTabs'; import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; -function TypesPurchase() { +function Purchase() { useScrollToHash(); return (
    -

    Purchase Types

    -

    - Type definitions for purchase transactions and active subscriptions. -

    - - - - - +

    Purchase

    Purchase

    Represents a completed or pending purchase transaction. The type is a - union of PurchaseIOS and PurchaseAndroid, - discriminated by the platform field. + union of{' '} + + PurchaseIOS + {' '} + and{' '} + + PurchaseAndroid + + , discriminated by the platform field. +

    +

    + Native references:{' '} + + Apple · StoreKit Transaction + + {' · '} + + Google · Purchase +

    PurchaseState

    Enum representing the current state of a purchase:

    + @@ -107,9 +98,9 @@ function TypesPurchase() { Note: iOS StoreKit 2 only returns Transaction objects on successful purchases, so iOS purchases always have{' '} Purchased state. See{' '} - + release notes - {' '} + {' '} for details.

    @@ -172,15 +163,10 @@ function TypesPurchase() { @@ -210,9 +196,9 @@ function TypesPurchase() { "premium"). On iOS: productId (e.g., "com.example.premium_monthly"). ⚠️ Android: May be inaccurate for multi-plan subscriptions. See{' '} - + limitation - + . @@ -323,6 +309,20 @@ function TypesPurchase() { + + + + + + + + @@ -392,104 +395,13 @@ function TypesPurchase() {
    - platform{' '} - - (deprecated) - + platform - Use store instead + Deprecated. Use store instead.
    Ownership type (purchased, family shared)
    + reasonIOS + + StoreKit 2 transaction reason (StoreKit raw value) +
    + reasonStringRepresentationIOS + String representation of the reason value
    transactionReasonIOS @@ -373,8 +373,11 @@ function TypesPurchase() { renewalInfoIOS - Subscription renewal information (see RenewalInfoIOS - below) + Subscription renewal information — see{' '} + + RenewalInfoIOS + + .
    -
    - - RenewalInfoIOS{' '} - - (from{' '} - - Product.SubscriptionInfo.RenewalInfo - - ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - willAutoRenew - Whether subscription will automatically renew
    - autoRenewPreference - - Product ID the subscription will renew to (may differ - if upgrade/downgrade pending) -
    - expirationReason - - Why subscription expired: "VOLUNTARY", - "BILLING_ERROR", "DID_NOT_AGREE_TO_PRICE_INCREASE", - "PRODUCT_NOT_AVAILABLE", "UNKNOWN" -
    - gracePeriodExpirationDate - Grace period end timestamp (epoch ms)
    - isInBillingRetry - True if retrying after billing failure
    - pendingUpgradeProductId - Product ID for pending upgrade/downgrade
    - priceIncreaseStatus - - Price increase response: "AGREED", "PENDING", or null -
    - renewalDate - Expected renewal timestamp (epoch ms)
    - renewalOfferId - Offer ID for next renewal
    - renewalOfferType - - Offer type: "PROMOTIONAL", "SUBSCRIPTION_OFFER_CODE", - "WIN_BACK" -
    -
    +

    + renewalInfoIOS resolves to{' '} + + RenewalInfoIOS + {' '} + — see that page for the full field reference. +

    @@ -745,250 +657,8 @@ function TypesPurchase() { }}
    - -
    - - ActiveSubscription - -

    - Represents an active subscription returned by{' '} - getActiveSubscriptions(). Provides a unified view of - subscription status across platforms. -

    - - - Common Fields - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - productId - Subscription product identifier
    - isActive - Whether the subscription is currently active
    - - willExpireSoon - {' '} - deprecated - - iOS only - returns null on Android. Use{' '} - daysUntilExpirationIOS for more precise control. -
    - transactionId - Transaction identifier for backend validation
    - purchaseToken - - JWS token (iOS) or purchase token (Android) for server - validation -
    - transactionDate - Transaction timestamp (epoch ms)
    - currentPlanId - - Unified plan identifier. On Android: basePlanId (e.g., - "premium"). On iOS: productId (e.g., - "com.example.premium_monthly"). ⚠️ Android: May - be inaccurate for multi-plan subscriptions. See{' '} - - limitation - - . -
    - - - Platform-Specific Fields - - - {{ - ios: ( - <> - - ActiveSubscriptionIOS - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - expirationDateIOS - Expiration timestamp (epoch ms)
    - environmentIOS - Environment: "Sandbox" or "Production"
    - daysUntilExpirationIOS - Days until expiration
    - renewalInfoIOS - - Subscription renewal details (see{' '} - RenewalInfoIOS) -
    - - ), - android: ( - <> - - ActiveSubscriptionAndroid - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - autoRenewingAndroid - Whether subscription will auto-renew
    - basePlanIdAndroid - - Base plan identifier. ⚠️ May be - inaccurate for multi-plan subscriptions. See{' '} - - limitation - - . -
    - purchaseTokenAndroid - Purchase token for upgrade/downgrade operations
    - - ), - }} -
    - - - Usage Example - - - {{ - typescript: ( - {`// Check for pending upgrades -if (subscription.renewalInfoIOS?.pendingUpgradeProductId) { - console.log('Upgrade pending to:', subscription.renewalInfoIOS.pendingUpgradeProductId); -} - -// Check if subscription is cancelled -if (subscription.renewalInfoIOS?.willAutoRenew === false) { - console.log('Subscription will not auto-renew'); -}`} - ), - swift: ( - {`// Check for pending upgrades -if let pendingProductId = subscription.renewalInfoIOS?.pendingUpgradeProductId { - print("Upgrade pending to: \\(pendingProductId)") -} - -// Check if subscription is cancelled -if subscription.renewalInfoIOS?.willAutoRenew == false { - print("Subscription will not auto-renew") -}`} - ), - kotlin: ( - {`// Check for pending upgrades -subscription.renewalInfoIOS?.pendingUpgradeProductId?.let { pendingProductId -> - println("Upgrade pending to: $pendingProductId") -} - -// Check if subscription is cancelled -if (subscription.renewalInfoIOS?.willAutoRenew == false) { - println("Subscription will not auto-renew") -}`} - ), - dart: ( - {`// Check for pending upgrades -if (subscription.renewalInfoIOS?.pendingUpgradeProductId != null) { - print('Upgrade pending to: \${subscription.renewalInfoIOS!.pendingUpgradeProductId}'); -} - -// Check if subscription is cancelled -if (subscription.renewalInfoIOS?.willAutoRenew == false) { - print('Subscription will not auto-renew'); -}`} - ), - gdscript: ( - {`# Check for pending upgrades -if subscription.renewal_info_ios != null: - if subscription.renewal_info_ios.pending_upgrade_product_id != "": - print("Upgrade pending to: %s" % subscription.renewal_info_ios.pending_upgrade_product_id) - -# Check if subscription is cancelled -if subscription.renewal_info_ios != null: - if subscription.renewal_info_ios.will_auto_renew == false: - print("Subscription will not auto-renew")`} - ), - }} - -
    ); } -export default TypesPurchase; +export default Purchase; diff --git a/packages/docs/src/pages/docs/types/request.tsx b/packages/docs/src/pages/docs/types/request-purchase-props.tsx similarity index 67% rename from packages/docs/src/pages/docs/types/request.tsx rename to packages/docs/src/pages/docs/types/request-purchase-props.tsx index 60b0beeb0..28eb347d9 100644 --- a/packages/docs/src/pages/docs/types/request.tsx +++ b/packages/docs/src/pages/docs/types/request-purchase-props.tsx @@ -1,212 +1,81 @@ +import { Link } from 'react-router-dom'; import AnchorLink from '../../../components/AnchorLink'; import CodeBlock from '../../../components/CodeBlock'; import LanguageTabs from '../../../components/LanguageTabs'; import PlatformTabs from '../../../components/PlatformTabs'; import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; import { useScrollToHash } from '../../../hooks/useScrollToHash'; -function TypesRequest() { +function RequestPurchaseProps() { useScrollToHash(); return (
    -

    Request Types

    -

    Type definitions for requesting products and initiating purchases.

    - - - - - -
    - - ProductRequest - -

    - Parameters for fetching products from the store via{' '} - fetchProducts(). -

    - - - Fields - - - - - - - - - - - - - - - - - - -
    NameSummary
    - skus - Array of product identifiers to fetch
    - type - - Product type filter (optional): "in-app" (default),{' '} - "subs", or "all" -
    - - - Usage Example - - - {{ - typescript: ( - {`// Fetch in-app purchases (default) -const inappProducts = await fetchProducts({ skus: ["product1", "product2"] }); - -// Fetch only subscriptions -const subscriptions = await fetchProducts({ - skus: ["sub1", "sub2"], - type: "subs" -}); - -// Fetch all products (both in-app and subscriptions) -const allProducts = await fetchProducts({ - skus: ["product1", "sub1"], - type: "all" -});`} - ), - swift: ( - {`// Fetch in-app purchases (default) -let inappProducts = try await OpenIapModule.shared.fetchProducts( - ProductRequest(skus: ["product1", "product2"]) -) - -// Fetch only subscriptions -let subscriptions = try await OpenIapModule.shared.fetchProducts( - ProductRequest(skus: ["sub1", "sub2"], type: .subs) -) - -// Fetch all products (both in-app and subscriptions) -let allProducts = try await OpenIapModule.shared.fetchProducts( - ProductRequest(skus: ["product1", "sub1"], type: .all) -)`} - ), - kotlin: ( - {`// Fetch in-app purchases (default) -val inappProducts = openIapStore.fetchProducts( - ProductRequest(skus = listOf("product1", "product2")) -) - -// Fetch only subscriptions -val subscriptions = openIapStore.fetchProducts( - ProductRequest(skus = listOf("sub1", "sub2"), type = ProductQueryType.Subs) -) - -// Fetch all products (both in-app and subscriptions) -val allProducts = openIapStore.fetchProducts( - ProductRequest(skus = listOf("product1", "sub1"), type = ProductQueryType.All) -)`} - ), - dart: ( - {`// Fetch in-app purchases (default) -final inappProducts = await FlutterInappPurchase.instance.fetchProducts( - skus: ['product1', 'product2'], -); - -// Fetch only subscriptions -final subscriptions = await FlutterInappPurchase.instance.fetchProducts( - skus: ['sub1', 'sub2'], - type: ProductQueryType.subs, -); - -// Fetch all products (both in-app and subscriptions) -final allProducts = await FlutterInappPurchase.instance.fetchProducts( - skus: ['product1', 'sub1'], - type: ProductQueryType.all, -);`} - ), - gdscript: ( - {`# Fetch in-app purchases (default) -var request = ProductRequest.new() -request.skus = ["product1", "product2"] -var inapp_products = await iap.fetch_products(request) - -# Fetch only subscriptions -var subs_request = ProductRequest.new() -subs_request.skus = ["sub1", "sub2"] -subs_request.type = ProductQueryType.SUBS -var subscriptions = await iap.fetch_products(subs_request) - -# Fetch all products (both in-app and subscriptions) -var all_request = ProductRequest.new() -all_request.skus = ["product1", "sub1"] -all_request.type = ProductQueryType.ALL -var all_products = await iap.fetch_products(all_request)`} - ), - }} - -
    - +

    RequestPurchaseProps

    Request Types

    Types used when initiating purchases via{' '} - requestPurchase(). + + requestPurchase() + + . +

    +

    + Native references:{' '} + + Apple · Product.purchase(options:) + + {' · '} + + Google · BillingFlowParams +

    RequestPurchaseProps

    - Top-level arguments for requestPurchase(). Wraps - platform-specific props with a type discriminator. + Top-level arguments for{' '} + + requestPurchase() + + . Wraps platform-specific props with a type discriminator.

    + + + @@ -215,7 +84,27 @@ var all_products = await iap.fetch_products(all_request)`}type + + + + + + @@ -229,7 +118,7 @@ var all_products = await iap.fetch_products(all_request)`} typescript: ( {`// Standard in-app purchase await requestPurchase({ - params: { + request: { apple: { sku: 'premium' }, google: { skus: ['premium'] } }, @@ -238,7 +127,7 @@ await requestPurchase({ // Subscription purchase await requestPurchase({ - params: { + request: { apple: { sku: 'monthly_sub' }, google: { skus: ['monthly_sub'] } }, @@ -295,7 +184,7 @@ await FlutterInappPurchase.instance.requestPurchase( apple: RequestPurchaseIosProps(sku: 'premium'), google: RequestPurchaseAndroidProps(skus: ['premium']), ), - type: ProductQueryType.inApp, + type: ProductQueryType.InApp, ), ); @@ -362,28 +251,18 @@ await iap.request_purchase(subs_props)`} @@ -419,28 +298,18 @@ await iap.request_purchase(subs_props)`} @@ -585,6 +454,19 @@ await iap.request_purchase(subs_props)`} + + + +
    NameType Summary
    - params + request + + + RequestPurchasePropsByPlatforms + Platform-specific purchase parameters (see below)
    - Purchase type: "in-app" or "subs" + "in-app" | "subs" + Purchase type discriminator
    + useAlternativeBilling + + boolean? + + Deprecated. Use{' '} + + enableBillingProgramAndroid + {' '} + in{' '} + + InitConnectionConfig + {' '} + instead. This flag only logs debug info and has no effect.
    - ios{' '} - - (deprecated) - + ios - Use apple instead + Deprecated. Use apple instead.
    - android{' '} - - (deprecated) - + android - Use google instead + Deprecated. Use google instead.
    - ios{' '} - - (deprecated) - + ios - Use apple instead + Deprecated. Use apple instead.
    - android{' '} - - (deprecated) - + android - Use google instead + Deprecated. Use google instead.
    True if offer is personalized (EU compliance)
    + developerBillingOption + + Developer billing option params for the External + Payments flow (8.3.0+). See{' '} + + DeveloperBillingOptionParamsAndroid + + . +
    @@ -603,9 +485,49 @@ await iap.request_purchase(subs_props)`} RequestSubscriptionIosProps

    - iOS subscriptions use the same props as regular purchases - (RequestPurchaseIosProps). + iOS subscriptions extend{' '} + + RequestPurchaseIosProps + {' '} + with these additional subscription-only fields:

    + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + winBackOffer + + Win-back offer to re-engage churned subscribers (iOS + 18+). +
    + promotionalOfferJWS + + JWS-signed promotional offer (iOS 15+, WWDC 2025). +
    + introductoryOfferEligibility + + Override introductory offer eligibility (iOS 15+, WWDC + 2025). Pass true/false to + force, omit to let the system decide. +
    ), android: ( @@ -633,10 +555,14 @@ await iap.request_purchase(subs_props)`} - replacementMode + + replacementMode + - How to handle subscription change (proration mode) + Deprecated. Use{' '} + subscriptionProductReplacementParams for + item-level replacement (Billing Library 8.1.0+). @@ -648,6 +574,28 @@ await iap.request_purchase(subs_props)`} sku, offerToken + + + subscriptionProductReplacementParams + + + Item-level replacement params for subscription + upgrades/downgrades (Billing Library 8.1.0+). + + + + + developerBillingOption + + + Developer billing option params (External Payments, + 8.3.0+). See{' '} + + DeveloperBillingOptionParamsAndroid + + . + + @@ -659,4 +607,4 @@ await iap.request_purchase(subs_props)`} ); } -export default TypesRequest; +export default RequestPurchaseProps; diff --git a/packages/docs/src/pages/docs/types/storefront.tsx b/packages/docs/src/pages/docs/types/storefront.tsx new file mode 100644 index 000000000..2b4c2332b --- /dev/null +++ b/packages/docs/src/pages/docs/types/storefront.tsx @@ -0,0 +1,86 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function Storefront() { + useScrollToHash(); + + return ( +
    + +

    Storefront

    +
    + + Storefront + +

    + Note: Storefront is not a struct in the + OpenIAP GraphQL schema. The schema defines{' '} + getStorefront: String!, so the value returned is a plain + ISO 3166-1 alpha-2 country-code string. This page exists as a + conceptual reference for the value returned by{' '} + + getStorefront() + + . +

    +

    + Native references:{' '} + + Apple · StoreKit Storefront + + {' · '} + + Google · BillingConfig.getCountryCode() + +

    + +

    Return shape

    + + + + + + + + + + + + + +
    TypeSummary
    + String! + + ISO 3166-1 alpha-2 country code (e.g. "US",{' '} + "KR", "JP"). Empty string when the + storefront cannot be determined. +
    + +
    +

    + iOS sources the value from the active StoreKit storefront. Android + queries Google Play Billing configuration and returns the same + country code string when available. +

    +
    +
    +
    + ); +} + +export default Storefront; diff --git a/packages/docs/src/pages/docs/types/subscription-offer.tsx b/packages/docs/src/pages/docs/types/subscription-offer.tsx new file mode 100644 index 000000000..7aa690e18 --- /dev/null +++ b/packages/docs/src/pages/docs/types/subscription-offer.tsx @@ -0,0 +1,557 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function SubscriptionOffer() { + useScrollToHash(); + + return ( +
    + +

    SubscriptionOffer

    +
    + + SubscriptionOffer + +

    + Standardized type for subscription promotional offers. Supported on + both iOS (introductory and promotional offers) and Android (offer + tokens with pricing phases). +

    +

    + Native references:{' '} + + Apple · Product.SubscriptionOffer + + {' · '} + + Google · ProductDetails.SubscriptionOfferDetails + +

    + + + Common Fields + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDescription
    + id + + ID! + Unique identifier for the offer
    + displayPrice + + String! + Formatted display price (e.g., "$9.99/month")
    + price + + Float! + Numeric price value
    + currency + + String + Currency code (ISO 4217)
    + type + + + DiscountOfferType! + + + Introductory, Promotional, or{' '} + WinBack (iOS 18+) +
    + period + + + SubscriptionPeriod + + Subscription period (unit + value)
    + periodCount + + Int + Number of periods the offer applies
    + paymentMode + + + PaymentMode + + FreeTrial, PayAsYouGo, or PayUpFront
    + + + iOS-Specific Fields + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDescription
    + keyIdentifierIOS + + String + Key ID for server-side signature validation
    + nonceIOS + + String + Cryptographic nonce (UUID) for signature
    + signatureIOS + + String + Server-generated signature for validation
    + timestampIOS + + Float + Timestamp when signature was generated
    + numberOfPeriodsIOS + + Int + Number of billing periods for this discount
    + localizedPriceIOS + + String + Localized price string
    + + + Android-Specific Fields + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDescription
    + basePlanIdAndroid + + String + Base plan identifier
    + offerTokenAndroid + + String + + Required for purchase. Pass to + requestPurchase() +
    + offerTagsAndroid + + [String!] + Tags associated with this offer
    + pricingPhasesAndroid + + + PricingPhasesAndroid + + Pricing phases (trial, intro, regular)
    + installmentPlanDetailsAndroid + + + InstallmentPlanDetailsAndroid + + + Installment plan details for subscription commitments (7.0+) +
    + + + Type Definition + + + {{ + typescript: ( + {`interface SubscriptionOffer { + // Common fields + id: string; + displayPrice: string; + price: number; + currency?: string; + type: DiscountOfferType; + period?: SubscriptionPeriod; + periodCount?: number; + paymentMode?: PaymentMode; + + // iOS-specific fields + keyIdentifierIOS?: string; + nonceIOS?: string; + signatureIOS?: string; + timestampIOS?: number; + numberOfPeriodsIOS?: number; + localizedPriceIOS?: string; + + // Android-specific fields + basePlanIdAndroid?: string; + offerTokenAndroid?: string; + offerTagsAndroid?: string[]; + pricingPhasesAndroid?: PricingPhasesAndroid; + installmentPlanDetailsAndroid?: InstallmentPlanDetailsAndroid; +} + +interface InstallmentPlanDetailsAndroid { + commitmentPaymentsCount: number; + subsequentCommitmentPaymentsCount: number; +} + +interface SubscriptionPeriod { + unit: SubscriptionPeriodUnit; + value: number; +} + +enum SubscriptionPeriodUnit { + Day = 'Day', + Week = 'Week', + Month = 'Month', + Year = 'Year', + Unknown = 'Unknown', +} + +enum PaymentMode { + FreeTrial = 'FreeTrial', + PayAsYouGo = 'PayAsYouGo', + PayUpFront = 'PayUpFront', + Unknown = 'Unknown', +}`} + ), + swift: ( + {`struct SubscriptionOffer: Codable { + // Common fields + let id: String + let displayPrice: String + let price: Double + let currency: String? + let type: DiscountOfferType + let period: SubscriptionPeriod? + let periodCount: Int? + let paymentMode: PaymentMode? + + // iOS-specific fields + let keyIdentifierIOS: String? + let nonceIOS: String? + let signatureIOS: String? + let timestampIOS: Double? + let numberOfPeriodsIOS: Int? + let localizedPriceIOS: String? + + // Android-specific fields + let basePlanIdAndroid: String? + let offerTokenAndroid: String? + let offerTagsAndroid: [String]? + let pricingPhasesAndroid: PricingPhasesAndroid? + let installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? +} + +struct InstallmentPlanDetailsAndroid: Codable { + let commitmentPaymentsCount: Int + let subsequentCommitmentPaymentsCount: Int +} + +struct SubscriptionPeriod: Codable { + let unit: SubscriptionPeriodUnit + let value: Int +} + +enum SubscriptionPeriodUnit: String, Codable { + case day = "Day" + case week = "Week" + case month = "Month" + case year = "Year" + case unknown = "Unknown" +} + +enum PaymentMode: String, Codable { + case freeTrial = "FreeTrial" + case payAsYouGo = "PayAsYouGo" + case payUpFront = "PayUpFront" + case unknown = "Unknown" +}`} + ), + kotlin: ( + {`data class SubscriptionOffer( + // Common fields + val id: String, + val displayPrice: String, + val price: Double, + val currency: String? = null, + val type: DiscountOfferType, + val period: SubscriptionPeriod? = null, + val periodCount: Int? = null, + val paymentMode: PaymentMode? = null, + + // iOS-specific fields + val keyIdentifierIOS: String? = null, + val nonceIOS: String? = null, + val signatureIOS: String? = null, + val timestampIOS: Double? = null, + val numberOfPeriodsIOS: Int? = null, + val localizedPriceIOS: String? = null, + + // Android-specific fields + val basePlanIdAndroid: String? = null, + val offerTokenAndroid: String? = null, + val offerTagsAndroid: List? = null, + val pricingPhasesAndroid: PricingPhasesAndroid? = null, + val installmentPlanDetailsAndroid: InstallmentPlanDetailsAndroid? = null +) + +data class InstallmentPlanDetailsAndroid( + val commitmentPaymentsCount: Int, + val subsequentCommitmentPaymentsCount: Int +) + +data class SubscriptionPeriod( + val unit: SubscriptionPeriodUnit, + val value: Int +) + +enum class SubscriptionPeriodUnit { + Day, Week, Month, Year, Unknown +} + +enum class PaymentMode { + FreeTrial, PayAsYouGo, PayUpFront, Unknown +}`} + ), + dart: ( + {`class SubscriptionOffer { + // Common fields + final String id; + final String displayPrice; + final double price; + final String? currency; + final DiscountOfferType type; + final SubscriptionPeriod? period; + final int? periodCount; + final PaymentMode? paymentMode; + + // iOS-specific fields + final String? keyIdentifierIOS; + final String? nonceIOS; + final String? signatureIOS; + final double? timestampIOS; + final int? numberOfPeriodsIOS; + final String? localizedPriceIOS; + + // Android-specific fields + final String? basePlanIdAndroid; + final String? offerTokenAndroid; + final List? offerTagsAndroid; + final PricingPhasesAndroid? pricingPhasesAndroid; + final InstallmentPlanDetailsAndroid? installmentPlanDetailsAndroid; + + SubscriptionOffer({ + required this.id, + required this.displayPrice, + required this.price, + this.currency, + required this.type, + this.period, + this.periodCount, + this.paymentMode, + this.keyIdentifierIOS, + this.nonceIOS, + this.signatureIOS, + this.timestampIOS, + this.numberOfPeriodsIOS, + this.localizedPriceIOS, + this.basePlanIdAndroid, + this.offerTokenAndroid, + this.offerTagsAndroid, + this.pricingPhasesAndroid, + this.installmentPlanDetailsAndroid, + }); +} + +class InstallmentPlanDetailsAndroid { + final int commitmentPaymentsCount; + final int subsequentCommitmentPaymentsCount; + + InstallmentPlanDetailsAndroid({ + required this.commitmentPaymentsCount, + required this.subsequentCommitmentPaymentsCount, + }); +} + +class SubscriptionPeriod { + final SubscriptionPeriodUnit unit; + final int value; + + SubscriptionPeriod({required this.unit, required this.value}); +} + +enum SubscriptionPeriodUnit { day, week, month, year, unknown } + +enum PaymentMode { freeTrial, payAsYouGo, payUpFront, unknown }`} + ), + gdscript: ( + {`class_name SubscriptionOffer + +# Common fields +var id: String +var display_price: String +var price: float +var currency: String +var type: DiscountOfferType +var period: SubscriptionPeriod +var period_count: int +var payment_mode: PaymentMode + +# iOS-specific fields +var key_identifier_ios: String +var nonce_ios: String +var signature_ios: String +var timestamp_ios: float +var number_of_periods_ios: int +var localized_price_ios: String + +# Android-specific fields +var base_plan_id_android: String +var offer_token_android: String +var offer_tags_android: Array[String] +var pricing_phases_android: PricingPhasesAndroid +var installment_plan_details_android: InstallmentPlanDetailsAndroid + +class InstallmentPlanDetailsAndroid: + var commitment_payments_count: int + var subsequent_commitment_payments_count: int + +class SubscriptionPeriod: + var unit: SubscriptionPeriodUnit + var value: int + +enum SubscriptionPeriodUnit { DAY, WEEK, MONTH, YEAR, UNKNOWN } +enum PaymentMode { FREE_TRIAL, PAY_AS_YOU_GO, PAY_UP_FRONT, UNKNOWN }`} + ), + }} + +
    +
    + ); +} + +export default SubscriptionOffer; diff --git a/packages/docs/src/pages/docs/types/subscription-product.tsx b/packages/docs/src/pages/docs/types/subscription-product.tsx new file mode 100644 index 000000000..4946a356a --- /dev/null +++ b/packages/docs/src/pages/docs/types/subscription-product.tsx @@ -0,0 +1,283 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import PlatformTabs from '../../../components/PlatformTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function SubscriptionProduct() { + useScrollToHash(); + + return ( +
    + +

    ProductSubscription

    +
    + + ProductSubscription + +

    + Represents a subscription product available for purchase. Extends the + base Product type with subscription-specific fields like pricing + phases, introductory offers, and billing periods. +

    +

    + Native references:{' '} + + Apple · Product.SubscriptionInfo + + {' · '} + + Google · ProductDetails.SubscriptionOfferDetails + +

    + + + Common Fields + +

    + Inherits every field from{' '} + + Product common fields + {' '} + (id, title, description,{' '} + displayName, displayPrice,{' '} + currency, price,{' '} + debugDescription,{' '} + platform ( + Deprecated.)), plus the subscription-only override + and the cross-platform offer arrays below. +

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + type + + "subs" + + Always "subs" for subscription products (overrides + the parent type discriminator). +
    + subscriptionOffers + + + SubscriptionOffer[] + + + Cross-platform offer list. Populated from StoreKit 2 promotional + offers on iOS and from Play Billing offer details on Android. +
    + discountOffers + + + DiscountOffer[] + + + Cross-platform discount list (introductory pricing, promo + codes). Always present in the schema; iOS-only stores may return + an empty array. +
    + + + Platform-Specific Fields + + + {{ + ios: ( + <> + + ProductSubscriptionIOS + +

    Additional fields available on iOS subscriptions:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + + discountsIOS + + + Deprecated. Use{' '} + subscriptionOffers instead. Array of + available discounts. Each contains:{' '} + identifier, type,{' '} + numberOfPeriods, price,{' '} + localizedPrice, paymentMode,{' '} + subscriptionPeriod +
    + introductoryPriceIOS + Formatted introductory price (e.g., "$0.99")
    + introductoryPriceAsAmountIOS + Numeric introductory price value
    + introductoryPricePaymentModeIOS + + Payment mode for intro offer (FreeTrial, PayAsYouGo, + PayUpFront) +
    + introductoryPriceNumberOfPeriodsIOS + Number of periods for intro pricing
    + introductoryPriceSubscriptionPeriodIOS + + Period unit for intro pricing (Day, Week, Month, Year) +
    + subscriptionPeriodNumberIOS + Number of units in a subscription period
    + subscriptionPeriodUnitIOS + Period unit (Day, Week, Month, Year)
    + typeIOS + + Detailed product type — for subscriptions this is almost + always AutoRenewableSubscription (or{' '} + NonRenewingSubscription). +
    + displayNameIOS + iOS-specific display name
    + isFamilyShareableIOS + Whether the subscription supports Family Sharing
    + jsonRepresentationIOS + Raw StoreKit 2 JWS payload
    + + ), + android: ( + <> + + ProductSubscriptionAndroid + +

    Additional fields available on Android subscriptions:

    + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + nameAndroid + Android-specific product name
    + productStatusAndroid + + Product fetch status code (OK,{' '} + NOT_FOUND, NO_OFFERS_AVAILABLE + , UNKNOWN) — Billing Library 8.0+ +
    + subscriptionOfferDetailsAndroid + + Array of subscription offers. Each contains:{' '} + basePlanId, offerId,{' '} + offerToken, pricingPhases,{' '} + offerTags +
    + + ), + }} +
    +
    +
    + ); +} + +export default SubscriptionProduct; diff --git a/packages/docs/src/pages/docs/types/verification.tsx b/packages/docs/src/pages/docs/types/verification.tsx deleted file mode 100644 index c49a96cbd..000000000 --- a/packages/docs/src/pages/docs/types/verification.tsx +++ /dev/null @@ -1,800 +0,0 @@ -import AnchorLink from '../../../components/AnchorLink'; -import CodeBlock from '../../../components/CodeBlock'; -import LanguageTabs from '../../../components/LanguageTabs'; -import PlatformTabs from '../../../components/PlatformTabs'; -import SEO from '../../../components/SEO'; -import TLDRBox from '../../../components/TLDRBox'; -import { useScrollToHash } from '../../../hooks/useScrollToHash'; -import { IAPKIT_URL, trackIapKitClick } from '../../../lib/config'; - -function TypesVerification() { - useScrollToHash(); - - return ( -
    - -

    Verification Types

    -

    - Type definitions for purchase verification with{' '} - verifyPurchase() and{' '} - verifyPurchaseWithProvider(). -

    - - - - - -
    - - Purchase Verification Types - -

    - Types used with verifyPurchase() for server-side purchase - verification. -

    - - - VerifyPurchaseProps - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - apple - - Apple App Store verification options. Contains: sku -
    - google - - Google Play verification options. Contains: sku,{' '} - packageName, purchaseToken,{' '} - accessToken, isSub -
    - horizon - - Meta Horizon (Quest) verification options. Contains:{' '} - sku, userId, accessToken -
    - - - VerifyPurchaseResult - -

    - Union of VerifyPurchaseResultIOS,{' '} - VerifyPurchaseResultAndroid, and{' '} - VerifyPurchaseResultHorizon. -

    - - {{ - ios: ( - <> - - VerifyPurchaseResultIOS - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - isValid - Whether verification succeeded
    - receiptData - Raw App Store receipt data
    - jwsRepresentation - JWS-encoded transaction
    - latestTransaction - Most recent transaction for this product
    - - ), - android: ( - <> - - VerifyPurchaseResultAndroid (Google Play) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameSummary
    - autoRenewing - Whether subscription will auto-renew
    - betaProduct - True if beta/test product
    - cancelDate - Cancellation timestamp (null if active)
    - cancelReason - Reason for cancellation
    - freeTrialEndDate - Free trial end timestamp
    - gracePeriodEndDate - Grace period end timestamp
    - productId - Product identifier
    - productType - Product type
    - purchaseDate - Purchase timestamp
    - quantity - Purchase quantity
    - transactionId - Transaction identifier
    - renewalDate - Next renewal timestamp
    - term - Subscription term (e.g., "P1M")
    - testTransaction - True if test/sandbox transaction
    - - - VerifyPurchaseResultHorizon (Meta Quest) - - - - - - - - - - - - - - - - - - -
    NameSummary
    - success - Whether the entitlement verification succeeded
    - grantTime - - Unix timestamp when the entitlement was granted (null if - verification failed) -
    - - ), - }} -
    -
    - -
    - - VerifyPurchaseWithProviderProps - -

    - Input type for verifyPurchaseWithProvider() - used to - verify purchases through external providers like{' '} - - IAPKit - - . -

    - - - - - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - provider - - PurchaseVerificationProvider - - The verification provider to use. Currently only{' '} - 'iapkit' is supported. -
    - iapkit - - RequestVerifyPurchaseWithIapkitProps? - IAPKit-specific verification parameters.
    - - - RequestVerifyPurchaseWithIapkitProps - -

    Parameters for IAPKit verification.

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - apiKey - - string? - - API key used for the Authorization header (Bearer {'{apiKey}'} - ). -
    - apple - - RequestVerifyPurchaseWithIapkitAppleProps? - Apple/iOS verification parameters.
    - google - - RequestVerifyPurchaseWithIapkitGoogleProps? - Google/Android verification parameters.
    - - - RequestVerifyPurchaseWithIapkitAppleProps - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - jws - - string - The JWS token returned with the purchase response.
    - - - RequestVerifyPurchaseWithIapkitGoogleProps - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - purchaseToken - - string - - The token provided to the user's device when the product or - subscription was purchased. -
    -
    - -
    - - VerifyPurchaseWithProviderResult - -

    - Result type returned by verifyPurchaseWithProvider(). -

    - - - - - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - provider - - PurchaseVerificationProvider - The provider used for verification.
    - iapkit - - RequestVerifyPurchaseWithIapkitResult? - IAPKit verification result (optional).
    - - - RequestVerifyPurchaseWithIapkitResult - -

    Individual verification result from IAPKit.

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    NameTypeSummary
    - store - - IapkitStore - - The store that processed the purchase ('apple' or{' '} - 'google'). -
    - isValid - - boolean - Whether the purchase is valid (not falsified).
    - state - - IapkitPurchaseState - The current state of the purchase.
    - - - IapkitPurchaseState - -

    Unified purchase states from IAPKit verification response.

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    ValueSummary
    - 'entitled' - - User is entitled to the product (purchase is complete and - active). -
    - 'pending-acknowledgment' - Purchase needs acknowledgment (Android only).
    - 'pending' - Purchase is pending completion.
    - 'canceled' - Purchase was canceled by the user.
    - 'expired' - Subscription has expired.
    - 'ready-to-consume' - Consumable purchase is ready to be consumed.
    - 'consumed' - Consumable product has been consumed.
    - 'unknown' - Purchase state could not be determined.
    - 'inauthentic' - - Purchase failed authenticity validation (potentially - fraudulent). -
    - - - IapkitStore - -

    Enumeration of stores supported by IAPKit.

    - - - - - - - - - - - - - - - - - -
    ValueSummary
    - 'apple' - Apple App Store.
    - 'google' - Google Play Store.
    - - - PurchaseVerificationProvider - -

    Supported verification providers.

    - - - - - - - - - - - - - -
    ValueSummary
    - 'iapkit' - - - IAPKit - {' '} - - Server-side purchase verification service. -
    - - - Usage Example - - - {{ - typescript: ( - {`import { verifyPurchaseWithProvider } from 'openiap'; -import type { - VerifyPurchaseWithProviderProps, - VerifyPurchaseWithProviderResult, -} from 'openiap'; - -// iOS verification -const iosResult = await verifyPurchaseWithProvider({ - provider: 'iapkit', - iapkit: { - apiKey: 'your-iapkit-api-key', - apple: { - jws: purchase.purchaseToken, // JWS from StoreKit 2 - }, - }, -}); - -// Android verification -const androidResult = await verifyPurchaseWithProvider({ - provider: 'iapkit', - iapkit: { - apiKey: 'your-iapkit-api-key', - google: { - purchaseToken: purchase.purchaseToken, - }, - }, -}); - -// Check result -if (result.iapkit?.isValid && result.iapkit.state === 'entitled') { - // Grant entitlement to user - console.log(\`Valid purchase from \${result.iapkit.store}\`); -}`} - ), - swift: ( - {`import OpenIAP - -// Create verification props for iOS -let props = VerifyPurchaseWithProviderProps( - iapkit: RequestVerifyPurchaseWithIapkitProps( - apiKey: "your-iapkit-api-key", - apple: RequestVerifyPurchaseWithIapkitAppleProps( - jws: purchase.jwsRepresentationIOS ?? "" - ), - google: nil - ), - provider: .iapkit -) - -// Verify purchase -let result = try await store.verifyPurchaseWithProvider(props) - -// Check result -if let iapkit = result, iapkit.isValid && iapkit.state == .entitled { - // Grant entitlement to user - print("Valid purchase from \\(iapkit.store)") -}`} - ), - kotlin: ( - {`import dev.hyo.openiap.* - -// Create verification props for Android -val props = VerifyPurchaseWithProviderProps( - iapkit = RequestVerifyPurchaseWithIapkitProps( - apiKey = "your-iapkit-api-key", - apple = null, - google = RequestVerifyPurchaseWithIapkitGoogleProps( - purchaseToken = purchase.purchaseToken - ) - ), - provider = PurchaseVerificationProvider.Iapkit -) - -// Verify purchase -val result = module.verifyPurchaseWithProvider(props) - -// Check result -result.iapkit?.let { iapkit -> - if (iapkit.isValid && iapkit.state == IapkitPurchaseState.Entitled) { - // Grant entitlement to user - println("Valid purchase from \${iapkit.store}") - } -}`} - ), - dart: ( - {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; - -// Create verification props for iOS -final props = VerifyPurchaseWithProviderProps( - provider: PurchaseVerificationProvider.iapkit, - iapkit: RequestVerifyPurchaseWithIapkitProps( - apiKey: 'your-iapkit-api-key', - apple: RequestVerifyPurchaseWithIapkitAppleProps( - jws: purchase.jwsRepresentationIOS ?? '', - ), - ), -); - -// Verify purchase -final result = await iap.verifyPurchaseWithProvider(props); - -// Check result -final iapkit = result.iapkit; -if (iapkit != null && iapkit.isValid && iapkit.state == IapkitPurchaseState.entitled) { - // Grant entitlement to user - print('Valid purchase from \${iapkit.store}'); -}`} - ), - gdscript: ( - {`# Create verification props for iOS -var props = VerifyPurchaseWithProviderProps.new() -props.provider = PurchaseVerificationProvider.IAPKIT -props.iapkit = RequestVerifyPurchaseWithIapkitProps.new() -props.iapkit.api_key = "your-iapkit-api-key" -props.iapkit.apple = RequestVerifyPurchaseWithIapkitAppleProps.new() -props.iapkit.apple.jws = purchase.jws_representation_ios - -# Verify purchase -var result = await iap.verify_purchase_with_provider(props) - -# Check result -var iapkit = result.iapkit -if iapkit != null and iapkit.is_valid and iapkit.state == IapkitPurchaseState.ENTITLED: - # Grant entitlement to user - print("Valid purchase from %s" % iapkit.store)`} - ), - }} - -
    -
    - ); -} - -export default TypesVerification; 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 new file mode 100644 index 000000000..0fc53fa36 --- /dev/null +++ b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-props.tsx @@ -0,0 +1,176 @@ +import AnchorLink from '../../../components/AnchorLink'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; +import { IAPKIT_URL, trackIapKitClick } from '../../../lib/config'; + +function VerifyPurchaseWithProviderProps() { + useScrollToHash(); + + return ( +
    + +

    VerifyPurchaseWithProviderProps

    +
    + + VerifyPurchaseWithProviderProps + +

    + Input type for verifyPurchaseWithProvider() - used to + verify purchases through external providers like{' '} + + IAPKit + + . +

    + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + provider + + PurchaseVerificationProvider + + The verification provider to use. Currently only{' '} + 'iapkit' is supported. +
    + iapkit + + RequestVerifyPurchaseWithIapkitProps? + IAPKit-specific verification parameters.
    + + + RequestVerifyPurchaseWithIapkitProps + +

    Parameters for IAPKit verification.

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + apiKey + + string? + + API key used for the Authorization header (Bearer {'{apiKey}'} + ). +
    + apple + + RequestVerifyPurchaseWithIapkitAppleProps? + Apple/iOS verification parameters.
    + google + + RequestVerifyPurchaseWithIapkitGoogleProps? + Google/Android verification parameters.
    + + + RequestVerifyPurchaseWithIapkitAppleProps + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + jws + + string + The JWS token returned with the purchase response.
    + + + RequestVerifyPurchaseWithIapkitGoogleProps + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + purchaseToken + + string + + The token provided to the user's device when the product or + subscription was purchased. +
    +
    +
    + ); +} + +export default VerifyPurchaseWithProviderProps; 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 new file mode 100644 index 000000000..d1d94d03c --- /dev/null +++ b/packages/docs/src/pages/docs/types/verify-purchase-with-provider-result.tsx @@ -0,0 +1,419 @@ +import { Link } from 'react-router-dom'; +import AnchorLink from '../../../components/AnchorLink'; +import CodeBlock from '../../../components/CodeBlock'; +import LanguageTabs from '../../../components/LanguageTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function VerifyPurchaseWithProviderResult() { + useScrollToHash(); + + return ( +
    + +

    VerifyPurchaseWithProviderResult

    +
    + + VerifyPurchaseWithProviderResult + +

    + Result type returned by verifyPurchaseWithProvider(). +

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + provider + + + PurchaseVerificationProvider + + The provider used for verification.
    + iapkit + + RequestVerifyPurchaseWithIapkitResult? + IAPKit verification result (optional).
    + errors + + VerifyPurchaseWithProviderError[]? + Error details if verification failed (see below).
    + + + VerifyPurchaseWithProviderError + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + message + + string + Human-readable error description
    + code + + string? + Optional machine-readable error code
    + + + RequestVerifyPurchaseWithIapkitResult + +

    Individual verification result from IAPKit.

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeSummary
    + store + + IapkitStore + + The store that processed the purchase ('apple' or{' '} + 'google'). +
    + isValid + + boolean + Whether the purchase is valid (not falsified).
    + state + + IapkitPurchaseState + The current state of the purchase.
    + + + IapkitPurchaseState + +

    Unified purchase states from IAPKit verification response.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ValueSummary
    + 'entitled' + + User is entitled to the product (purchase is complete and + active). +
    + 'pending-acknowledgment' + Purchase needs acknowledgment (Android only).
    + 'pending' + Purchase is pending completion.
    + 'canceled' + Purchase was canceled by the user.
    + 'expired' + Subscription has expired.
    + 'ready-to-consume' + Consumable purchase is ready to be consumed.
    + 'consumed' + Consumable product has been consumed.
    + 'unknown' + Purchase state could not be determined.
    + 'inauthentic' + + Purchase failed authenticity validation (potentially + fraudulent). +
    + + + IapkitStore + +

    Enumeration of stores supported by IAPKit.

    + + + + + + + + + + + + + + + + + +
    ValueSummary
    + 'apple' + Apple App Store.
    + 'google' + Google Play Store.
    + + + PurchaseVerificationProvider + +

    Supported verification providers.

    + + + + + + + + + + + + + +
    ValueSummary
    + 'iapkit' + + + IAPKit + {' '} + - Server-side purchase verification service. +
    + + + Usage Example + + + {{ + typescript: ( + {`import { verifyPurchaseWithProvider } from 'openiap'; +import type { + VerifyPurchaseWithProviderProps, + VerifyPurchaseWithProviderResult, +} from 'openiap'; + +// iOS verification +const iosResult = await verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + apiKey: 'your-iapkit-api-key', + apple: { + jws: purchase.purchaseToken, // JWS from StoreKit 2 + }, + }, +}); + +// Android verification +const androidResult = await verifyPurchaseWithProvider({ + provider: 'iapkit', + iapkit: { + apiKey: 'your-iapkit-api-key', + google: { + purchaseToken: purchase.purchaseToken, + }, + }, +}); + +// Check result +if (result.iapkit?.isValid && result.iapkit.state === 'entitled') { + // Grant entitlement to user + console.log(\`Valid purchase from \${result.iapkit.store}\`); +}`} + ), + swift: ( + {`import OpenIAP + +// Create verification props for iOS +let props = VerifyPurchaseWithProviderProps( + iapkit: RequestVerifyPurchaseWithIapkitProps( + apiKey: "your-iapkit-api-key", + apple: RequestVerifyPurchaseWithIapkitAppleProps( + jws: purchase.jwsRepresentationIOS ?? "" + ), + google: nil + ), + provider: .iapkit +) + +// Verify purchase +let result = try await store.verifyPurchaseWithProvider(props) + +// Check result +if let iapkit = result, iapkit.isValid && iapkit.state == .entitled { + // Grant entitlement to user + print("Valid purchase from \\(iapkit.store)") +}`} + ), + kotlin: ( + {`import dev.hyo.openiap.* + +// Create verification props for Android +val props = VerifyPurchaseWithProviderProps( + iapkit = RequestVerifyPurchaseWithIapkitProps( + apiKey = "your-iapkit-api-key", + apple = null, + google = RequestVerifyPurchaseWithIapkitGoogleProps( + purchaseToken = purchase.purchaseToken + ) + ), + provider = PurchaseVerificationProvider.Iapkit +) + +// Verify purchase +val result = module.verifyPurchaseWithProvider(props) + +// Check result +result.iapkit?.let { iapkit -> + if (iapkit.isValid && iapkit.state == IapkitPurchaseState.Entitled) { + // Grant entitlement to user + println("Valid purchase from \${iapkit.store}") + } +}`} + ), + dart: ( + {`import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; + +// Create verification props for iOS +final props = VerifyPurchaseWithProviderProps( + provider: PurchaseVerificationProvider.iapkit, + iapkit: RequestVerifyPurchaseWithIapkitProps( + apiKey: 'your-iapkit-api-key', + apple: RequestVerifyPurchaseWithIapkitAppleProps( + jws: purchase.jwsRepresentationIOS ?? '', + ), + ), +); + +// Verify purchase +final result = await iap.verifyPurchaseWithProvider(props); + +// Check result +final iapkit = result.iapkit; +if (iapkit != null && iapkit.isValid && iapkit.state == IapkitPurchaseState.entitled) { + // Grant entitlement to user + print('Valid purchase from \${iapkit.store}'); +}`} + ), + gdscript: ( + {`# Create verification props for iOS +var props = VerifyPurchaseWithProviderProps.new() +props.provider = PurchaseVerificationProvider.IAPKIT +props.iapkit = RequestVerifyPurchaseWithIapkitProps.new() +props.iapkit.api_key = "your-iapkit-api-key" +props.iapkit.apple = RequestVerifyPurchaseWithIapkitAppleProps.new() +props.iapkit.apple.jws = purchase.jws_representation_ios + +# Verify purchase +var result = await iap.verify_purchase_with_provider(props) + +# Check result +var iapkit = result.iapkit +if iapkit != null and iapkit.is_valid and iapkit.state == IapkitPurchaseState.ENTITLED: + # Grant entitlement to user + print("Valid purchase from %s" % iapkit.store)`} + ), + }} + +
    +
    + ); +} + +export default VerifyPurchaseWithProviderResult; diff --git a/packages/docs/src/pages/docs/types/verify-purchase.tsx b/packages/docs/src/pages/docs/types/verify-purchase.tsx new file mode 100644 index 000000000..36d4b3a10 --- /dev/null +++ b/packages/docs/src/pages/docs/types/verify-purchase.tsx @@ -0,0 +1,319 @@ +import AnchorLink from '../../../components/AnchorLink'; +import PlatformTabs from '../../../components/PlatformTabs'; +import SEO from '../../../components/SEO'; +import { useScrollToHash } from '../../../hooks/useScrollToHash'; + +function VerifyPurchase() { + useScrollToHash(); + + return ( +
    + +

    VerifyPurchase Types

    +
    + + Purchase Verification Types + +

    + Types used with verifyPurchase() for server-side purchase + verification. +

    +

    + Native references:{' '} + + Apple · App Store Server API + + {' · '} + + Google Play Developer API · purchases.subscriptionsv2 + + {' · '} + + Meta Horizon · IAP Overview + +

    + + + VerifyPurchaseProps + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + apple + + Apple App Store verification options. Contains: sku +
    + google + + Google Play verification options. Contains: sku,{' '} + packageName, purchaseToken,{' '} + accessToken, isSub +
    + horizon + + Meta Horizon (Quest) verification options. Contains:{' '} + sku, userId, accessToken +
    + + + VerifyPurchaseResult + +

    + Union of VerifyPurchaseResultIOS,{' '} + VerifyPurchaseResultAndroid, and{' '} + VerifyPurchaseResultHorizon. +

    + + {{ + ios: ( + <> + + VerifyPurchaseResultIOS + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + isValid + Whether verification succeeded
    + receiptData + Raw App Store receipt data
    + jwsRepresentation + JWS-encoded transaction
    + latestTransaction + Most recent transaction for this product
    + + ), + android: ( + <> + + VerifyPurchaseResultAndroid (Google Play) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameSummary
    + autoRenewing + Whether subscription will auto-renew
    + betaProduct + True if beta/test product
    + cancelDate + Cancellation timestamp (null if active)
    + cancelReason + Reason for cancellation
    + deferredDate + + Deferred replacement date (when an upgrade/downgrade + will take effect) +
    + deferredSku + SKU the subscription will switch to on deferral
    + freeTrialEndDate + Free trial end timestamp
    + gracePeriodEndDate + Grace period end timestamp
    + parentProductId + + Parent subscription product ID (when this purchase is a + base-plan child) +
    + productId + Product identifier
    + productType + Product type
    + receiptId + Google Play receipt identifier
    + purchaseDate + Purchase timestamp
    + quantity + Purchase quantity
    + transactionId + Transaction identifier
    + renewalDate + Next renewal timestamp
    + term + Subscription term (e.g., "P1M")
    + termSku + SKU associated with the subscription term
    + testTransaction + True if test/sandbox transaction
    + + + VerifyPurchaseResultHorizon (Meta Quest) + + + + + + + + + + + + + + + + + + +
    NameSummary
    + success + Whether the entitlement verification succeeded
    + grantTime + + Unix timestamp when the entitlement was granted (null if + verification failed) +
    + + ), + }} +
    +
    +
    + ); +} + +export default VerifyPurchase; diff --git a/packages/docs/src/pages/docs/updates/announcements.tsx b/packages/docs/src/pages/docs/updates/announcements.tsx index f06ce2857..22aa98eac 100644 --- a/packages/docs/src/pages/docs/updates/announcements.tsx +++ b/packages/docs/src/pages/docs/updates/announcements.tsx @@ -463,7 +463,7 @@ function Announcements() { verifyPurchaseWithProvider API with{' '} provider: 'iapkit'. See the{' '} API documentation diff --git a/packages/docs/src/pages/docs/updates/releases.tsx b/packages/docs/src/pages/docs/updates/releases.tsx index 52e6b8996..9d847974c 100644 --- a/packages/docs/src/pages/docs/updates/releases.tsx +++ b/packages/docs/src/pages/docs/updates/releases.tsx @@ -1,3 +1,4 @@ +import { Link } from 'react-router-dom'; import { useMemo } from 'react'; import SEO from '../../../components/SEO'; import { useScrollToHash, getHashId } from '../../../hooks/useScrollToHash'; @@ -410,8 +411,11 @@ function Releases() {
  • New advancedCommerceInfoIOS field on{' '} - PurchaseIOS — present only for transactions using - the Advanced Commerce API with generic SKU purchases. + + PurchaseIOS + {' '} + — present only for transactions using the Advanced Commerce API + with generic SKU purchases.
  • Contains item details, tax info, and refund data from{' '} @@ -433,8 +437,14 @@ function Releases() { }} >
  • - New getAllTransactionsIOS() query returns the full - StoreKit 2 transaction history as PurchaseIOS{' '} + New{' '} + + getAllTransactionsIOS() + {' '} + query returns the full StoreKit 2 transaction history as{' '} + + PurchaseIOS + {' '} values.
  • @@ -444,7 +454,11 @@ function Releases() {
  • Unlike getAvailablePurchases, always returns the - iOS-specific PurchaseIOS shape. + iOS-specific{' '} + + PurchaseIOS + {' '} + shape.
  • @@ -459,12 +473,10 @@ function Releases() { }} >
  • - - AdvancedCommerceInfoIOS Type - + AdvancedCommerceInfoIOS Type
  • - + getAllTransactionsIOS API
  • @@ -599,7 +611,10 @@ function Releases() { IapEvent.SubscriptionBillingIssue enum value and{' '} subscriptionBillingIssue: Purchase! subscription in{' '} event.graphql. Payload is the affected{' '} - Purchase. + + Purchase + + .
  • iOS: registered via{' '} @@ -933,8 +948,10 @@ function Releases() { subscriptionProductReplacementParams on Android {' '} — the field was declared on{' '} - RequestSubscriptionAndroidProps and parsed - correctly by the native plugin, but{' '} + + RequestSubscriptionAndroidProps + {' '} + and parsed correctly by the native plugin, but{' '} flutter_inapp_purchase.dart was dropping it when building the method-channel payload, so the native side received{' '} null and Google Play applied its default @@ -1599,7 +1616,7 @@ product.priceFormatStyle.locale.currencyCode`} }} > @@ -1607,7 +1624,7 @@ product.priceFormatStyle.locale.currencyCode`} DiscountOffer.purchaseOptionIdAndroid @@ -1643,7 +1660,7 @@ product.priceFormatStyle.locale.currencyCode`} }} > @@ -1651,7 +1668,7 @@ product.priceFormatStyle.locale.currencyCode`} SubscriptionOffer.installmentPlanDetailsAndroid @@ -1662,8 +1679,11 @@ product.priceFormatStyle.locale.currencyCode`} {/* Section 3: PendingPurchaseUpdateAndroid */} @@ -1832,7 +1849,10 @@ product.priceFormatStyle.locale.currencyCode`}
    - 3. Improved presentExternalPurchaseNoticeSheetIOS() + 3. Improved{' '} + + presentExternalPurchaseNoticeSheetIOS() +

    }} > Removed subscription-specific fields from{' '} - RequestPurchaseIosProps. These fields now only exist - in RequestSubscriptionIosProps. + + RequestPurchaseIosProps + + . These fields now only exist in{' '} + + RequestSubscriptionIosProps + + .

    • productStatusAndroid - New field on{' '} - ProductAndroid + + ProductAndroid +
    @@ -2414,14 +2442,24 @@ result.error // optional error`} color: 'var(--text-secondary)', }} > - Introduced standardized DiscountOffer and{' '} - SubscriptionOffer types for unified handling across iOS - and Android. + Introduced standardized{' '} + + DiscountOffer + {' '} + and{' '} + + SubscriptionOffer + {' '} + types for unified handling across iOS and Android.

    - 1. DiscountOffer (One-time products) + 1.{' '} + + DiscountOffer + {' '} + (One-time products)
      - 2. SubscriptionOffer + 2.{' '} + + SubscriptionOffer +
      • Replaces deprecated{' '} ProductSubscriptionAndroidOfferDetails,{' '} - DiscountOfferIOS, DiscountIOS + + DiscountOfferIOS + + ,{' '} + + DiscountIOS +
      @@ -2525,13 +2572,19 @@ result.error // optional error`} ProductAndroidOneTimePurchaseOfferDetail {' '} - → DiscountOffer + →{' '} + + DiscountOffer +
    • ProductSubscriptionAndroidOfferDetails {' '} - → SubscriptionOffer + →{' '} + + SubscriptionOffer +
    • @@ -2644,8 +2697,15 @@ result.error // optional error`} color: 'var(--text-secondary)', }} > - Deprecated AlternativeBillingModeAndroid in favor of - unified BillingProgramAndroid enum. + Deprecated{' '} + + AlternativeBillingModeAndroid + {' '} + in favor of unified{' '} + + BillingProgramAndroid + {' '} + enum.

      • - AlternativeBillingModeAndroid + + AlternativeBillingModeAndroid + {' '} - Deprecated
      • @@ -2778,7 +2840,10 @@ result.error // optional error`} Added{' '} enableBillingProgramAndroid: BillingProgramAndroid{' '} field for easier billing program setup during{' '} - initConnection(). + + initConnection() + + .

    @@ -2794,8 +2859,11 @@ result.error // optional error`} }} > All API methods now automatically call{' '} - initConnection() internally. No need to manually call - it before using any API. Backward compatible. + + initConnection() + {' '} + internally. No need to manually call it before using any API. + Backward compatible.

  • @@ -3094,8 +3162,14 @@ result.error // optional error`}
    }} >
  • - New optional field in RequestPurchaseIosProps and{' '} - RequestSubscriptionIosProps + New optional field in{' '} + + RequestPurchaseIosProps + {' '} + and{' '} + + RequestSubscriptionIosProps +
  • Use cases: Campaign attribution, affiliate marketing, @@ -3106,7 +3180,10 @@ result.error // optional error`}
    - 2. Deprecated requestPurchaseOnPromotedProductIOS() + 2. Deprecated{' '} + + requestPurchaseOnPromotedProductIOS() +

    }} > In StoreKit 2, use promotedProductListenerIOS +{' '} - requestPurchase() directly. + + requestPurchase() + {' '} + directly.

    @@ -3236,7 +3316,10 @@ result.error // optional error`}

    - See: verifyPurchase API + See:{' '} + + verifyPurchase API +

    ), @@ -3481,9 +3564,17 @@ result.error // optional error`} }} >
  • - canPresentExternalPurchaseNoticeIOS(),{' '} - presentExternalPurchaseNoticeSheetIOS(),{' '} - presentExternalPurchaseLinkIOS() + + canPresentExternalPurchaseNoticeIOS() + + ,{' '} + + presentExternalPurchaseNoticeSheetIOS() + + ,{' '} + + presentExternalPurchaseLinkIOS() +
  • @@ -3507,9 +3598,15 @@ result.error // optional error`}
    color: 'var(--text-secondary)', }} > - New standardized APIs: getActiveSubscriptions(),{' '} - hasActiveSubscriptions() - automatic detection without - requiring product IDs. + New standardized APIs:{' '} + + getActiveSubscriptions() + + ,{' '} + + hasActiveSubscriptions() + {' '} + - automatic detection without requiring product IDs.

    ), diff --git a/packages/docs/src/pages/home.tsx b/packages/docs/src/pages/home.tsx index fb2519942..6ae484c0e 100644 --- a/packages/docs/src/pages/home.tsx +++ b/packages/docs/src/pages/home.tsx @@ -446,31 +446,31 @@ function Home() {

    Standard methods across all platforms

    - + initConnection() Initialize IAP service - + fetchProducts() Fetch product details - + requestPurchase() Initiate purchase flow - + finishTransaction() Complete purchase getAvailablePurchases() Restore entitlements getActiveSubscriptions() @@ -487,42 +487,42 @@ function Home() {
    purchaseUpdatedListener Purchase state changes purchaseErrorListener Error handling promotedProductListenerIOS App Store promoted products userChoiceBillingListenerAndroid User Choice Billing selection developerProvidedBillingListener External billing choice subscriptionBillingIssueListener @@ -538,31 +538,34 @@ function Home() {

    Common data structures for all platforms

    - + Product Product information - + Purchase Transaction details - + PurchaseError Error definitions - + SubscriptionPeriod Billing cycles ProductSubscription Subscription product shape ActiveSubscription diff --git a/packages/docs/src/pages/introduction.tsx b/packages/docs/src/pages/introduction.tsx index 87b43eed3..f968f2cb8 100644 --- a/packages/docs/src/pages/introduction.tsx +++ b/packages/docs/src/pages/introduction.tsx @@ -75,19 +75,19 @@ function Introduction() {
    • Unified API methods —{' '} - + initConnection() ,{' '} - + fetchProducts() ,{' '} - + requestPurchase() ,{' '} - + finishTransaction()
    • @@ -101,21 +101,21 @@ function Introduction() { Purchase ,{' '} - + SubscriptionPeriod ,{' '} - + PurchaseError
    • Standard event patterns —{' '} - + purchaseUpdatedListener ,{' '} - + purchaseErrorListener
    • @@ -251,7 +251,13 @@ src/generated/types.gd # GDScript types`} functionName - fetchProducts(), requestPurchase() + + fetchProducts() + + ,{' '} + + requestPurchase() + @@ -260,7 +266,13 @@ src/generated/types.gd # GDScript types`} functionNameIOS - syncIOS(), getStorefrontIOS() + + syncIOS() + + ,{' '} + + getStorefrontIOS() + @@ -269,7 +281,9 @@ src/generated/types.gd # GDScript types`} functionNameAndroid - acknowledgePurchaseAndroid() + + acknowledgePurchaseAndroid() + @@ -327,13 +341,13 @@ const subscription = purchaseUpdatedListener(async (purchase) => { // 5. Acknowledge the purchase // Android: auto-refunds after 3 days if not acknowledged - await finishTransaction(purchase, isConsumable); + await finishTransaction({ purchase, isConsumable }); }); // 6. Fetch products const products = await fetchProducts({ products: [ - { id: 'com.app.premium', type: 'inapp' }, + { id: 'com.app.premium', type: 'in-app' }, { id: 'com.app.monthly', type: 'subs' }, ], }); @@ -344,7 +358,7 @@ await requestPurchase({ apple: { sku: 'com.app.premium' }, google: { skus: ['com.app.premium'] }, }, - type: 'inapp', + type: 'in-app', }); // 8. Cleanup on unmount diff --git a/packages/docs/src/styles/base.css b/packages/docs/src/styles/base.css index 0edf996f0..9496cdc0b 100644 --- a/packages/docs/src/styles/base.css +++ b/packages/docs/src/styles/base.css @@ -11,7 +11,7 @@ html { background-color: #fefefe; background-color: var(--bg-primary); font-size: 16px; - overflow-x: hidden; + overflow-x: clip; width: 100%; max-width: 100vw; position: relative; @@ -39,14 +39,14 @@ body { transition: background-color 0.2s ease, color 0.2s ease; - overflow-x: hidden; + overflow-x: clip; width: 100%; max-width: 100vw; position: relative; } #root { - overflow-x: hidden; + overflow-x: clip; width: 100%; max-width: 100vw; position: relative; diff --git a/packages/docs/src/styles/documentation.css b/packages/docs/src/styles/documentation.css index 68fb652d6..cb167f130 100644 --- a/packages/docs/src/styles/documentation.css +++ b/packages/docs/src/styles/documentation.css @@ -6,18 +6,60 @@ min-height: calc(100vh - 56px); } -/* Sidebar - Light mode: light bg, Dark mode: dark bg */ +/* Sidebar - Light mode: light bg, Dark mode: dark bg. + The container is centered with `margin: 0 auto`, so on wide screens + there's a gap between the viewport's left edge and the sidebar's + start. We pull the sidebar leftward by exactly that gap so the + background extends to the viewport edge. The gap amount is exposed + as a CSS variable (`--sidebar-edge-fill`) and added to each nav + row's left padding so the active highlight overlay reaches the + edge as well — instead of starting at the original 280px column. */ .docs-sidebar { + --sidebar-edge-fill: max(0px, calc((100vw - var(--max-width)) / 2)); width: 280px; flex-shrink: 0; + align-self: flex-start; position: sticky; top: 56px; - height: calc(100vh - 56px); + max-height: calc(100vh - 56px); overflow-y: auto; overflow-x: hidden; + overscroll-behavior: contain; background: var(--bg-secondary); border-right: 1px solid var(--border-color); - padding: var(--spacing-lg) 0; + padding: var(--spacing-sm) 0 var(--spacing-lg); + margin-left: calc((var(--max-width) - 100vw) / 2); + box-sizing: content-box; +} + +/* Sidebar items: truncate long identifiers with an ellipsis so the + sidebar stays fixed-width and visually clean. The active item wraps + to its full label so the reader always sees the page they're on. */ +.docs-sidebar .menu-dropdown-item, +.docs-sidebar .menu-dropdown-title, +.docs-sidebar .docs-nav a { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.docs-sidebar .menu-dropdown-item.active, +.docs-sidebar .docs-nav a.active { + white-space: normal; + overflow: visible; + overflow-wrap: anywhere; + word-break: break-word; +} + +/* Hovering an inactive item briefly expands it so the full identifier + is reachable without having to navigate first. */ +.docs-sidebar .menu-dropdown-item:hover, +.docs-sidebar .docs-nav a:hover { + white-space: normal; + overflow: visible; + overflow-wrap: anywhere; + word-break: break-word; } :root.dark .docs-sidebar { @@ -93,7 +135,8 @@ .docs-nav a { color: var(--text-secondary); text-decoration: none; - padding: var(--spacing-sm) var(--spacing-lg); + padding: var(--spacing-sm) var(--spacing-lg) var(--spacing-sm) + calc(var(--sidebar-edge-fill, 0px) + var(--spacing-lg)); display: block; font-size: var(--font-size-sm); transition: all 0.15s ease; @@ -124,12 +167,24 @@ background: rgba(164, 116, 101, 0.15); } -/* Menu Dropdown styles for sidebar */ +/* Menu Dropdown styles for sidebar. + `.menu-dropdown-header` may render as either a
      (top-level + MenuDropdown — wraps a separate nav button + toggle button) or a +